Symfony 4. 为什么提交的表单只是部分填充模型?
Symfony 4. Why does submitted Form just partially populate the Model?
有点恐慌 - 我正在为复杂搜索生成 Symfony 表单,即映射到实体的数据将仅用于搜索查询构建。
我从 ChoiceType 创建了简单的表单、模型和一些扩展类型,用于通过某种逻辑进行预填充选择。使用 GET 方法提交表单。
例如,在模型中您会找到 maker
和 model
字段。后者在选择制造商后在前端填充 AJAX。当我提交表单时,maker
和 model
具有非默认值,handleRequest
仅填充模型的 maker
属性,但model
留空。如果选中,复选框也会正确填充。总而言之,$form->getData()
returns 只是 Maker 和复选框,其他字段为空。 $request->query
具有所有参数。
数据映射器在这里毫无意义。而且数据中也没有什么可以转换的,模型主要来自标量值。请求包含所有内容,但未正确处理。我尝试实现 ChoiceLoaderInterface
,但这对我不起作用,因为在加载选择期间我必须访问表单的 options
,而我没有(我使用这篇文章 https://speakerdeck.com/heahdude/symfony-forms-use-cases-and-optimization).
我正在使用 Symfony 4.2.4; PHP 7.2.
控制器的方法
/**
* @Route("/search/car", name="car_search", methods={"GET"})
* @param Request $request
*/
public function carSearchAction(Request $request)
{
$carModel = new CarSimpleSearchModel();
$form = $this->createForm(CarSimpleSearchType::class, $carModel);
$form->handleRequest($request);
$form->getData();
.....
}
CarSimpleSearchModel
class CarSimpleSearchModel
{
public $maker;
public $model;
public $priceFrom;
public $priceTo;
public $yearFrom;
public $yearTo;
public $isCompanyOwner;
public $isPrivateOwners;
public $isRoublePrice;
}
CarSimpleSearch键入表格
class CarSimpleSearchType extends AbstractType
{
protected $urlGenerator;
public function __construct(UrlGeneratorInterface $urlGenerator)
{
$this->urlGenerator = $urlGenerator;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('vehicle_type', HiddenType::class, [
'data' => VehicleTypeType::CAR,
'mapped' => false,
])
->add('maker', CarMakerSelectType::class)
->add('model', CarModelsSelectType::class)
->add(
'priceFrom',
VehiclePriceRangeType::class,
[
'vehicle_type' => VehicleTypeType::CAR,
]
)
->add(
'priceTo',
VehiclePriceRangeType::class,
[
'vehicle_type' => VehicleTypeType::CAR,
]
)
->add(
'yearFrom',
VehicleYearRangeType::class,
[
'vehicle_type' => VehicleTypeType::CAR,
]
)
->add(
'yearTo',
VehicleYearRangeType::class,
[
'vehicle_type' => VehicleTypeType::CAR,
]
)
->add('isCompanyOwner', CheckboxType::class)
->add('isPrivateOwners', CheckboxType::class)
->add('isRoublePrice', CheckboxType::class)
->add('submit', SubmitType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'data_class' => CarSimpleSearchModel::class,
'compound' => true,
'method' => 'GET',
'required' => false,
'action' => $this->urlGenerator->generate('car_search'),
]
);
}
public function getBlockPrefix()
{
return 'car_search_form';
}
}
CarMakerSelectType 字段
class CarMakerSelectType extends AbstractType
{
/**
* @var VehicleExtractorService
*/
private $extractor;
/**
* VehicleMakerSelectType constructor.
*
* @param VehicleExtractorService $extractor
*/
public function __construct(VehicleExtractorService $extractor)
{
$this->extractor = $extractor;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'placeholder' => null,
'vehicle_type' => null,
'choices' => $this->getVariants(),
]
);
}
private function getVariants()
{
$makers = $this->extractor->getMakersByVehicleType(VehicleTypeType::CAR);
$choices = [];
foreach ($makers as $maker) {
$choices[$maker['name']] = $maker['id'];
}
return $choices;
}
}
CarModelSelectType 字段
class CarModelsSelectType extends AbstractType
{
private $extractor;
public function __construct(VehicleExtractorService $extractor)
{
$this->extractor = $extractor;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'disabled' => true,
]
);
}
}
VehiclePriceRangeType 字段
class VehiclePriceRangeType extends AbstractType
{
private $extractor;
public function __construct(VehicleExtractorService $extractor)
{
$this->extractor = $extractor;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'vehicle_type' => null,
]
);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
foreach ($this->getRange($options['vehicle_type']) as $value) {
$view->vars['choices'][] = new ChoiceView($value, $value, $value);
}
}
private function getRange(int $vehicleType)
{
return PriceRangeGenerator::generate($this->extractor->getMaxVehiclePrice($vehicleType));
}
}
VehicleYearRangeType 字段
class VehicleYearRangeType extends AbstractType
{
private $extractor;
public function __construct(VehicleExtractorService $extractorService)
{
$this->extractor = $extractorService;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'vehicle_type' => null,
]
);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
foreach ($this->getRange($options['vehicle_type']) as $value) {
$view->vars['choices'][] = new ChoiceView($value, $value, $value);
}
}
protected function getRange(int $vehicleType): array
{
$yearRange = RangeGenerator::generate(
$this->extractor->getMinYear($vehicleType),
$this->extractor->getMaxYear($vehicleType),
1,
true,
true
);
return $yearRange;
}
}
所以,我可以使用来自 Request
的原始数据并手动验证填充模型并发送给进一步处理,但我想这不是正确的方法,我想通过以下方式填充表单框架。我该怎么办?..
在我的例子中,我有一个依赖 EntityType
填充的 ajax 最初是禁用的。由于 choices
其中 null
,它在提交时返回 InvalidValueException
。我必须做的是创建一个 EventListener
并为当前 'main' 字段添加有效的 choices
。基本上就是这样,或多或少适合你的情况。
原始形式:
// Setup Fields
$builder
->add('maker', CarMakerSelectType::class)
->add('model', CarModelsSelectType::class, [
'choices' => [],
// I was setting the disabled on a Event::PRE_SET_DATA if previous field was null
// since I could be loading values from the database but I guess you can do it here
'attr' => ['disabled' => 'disabled'],
]
);
$builder->addEventSubscriber(new ModelListener($this->extractor));
添加回有效选择的事件订阅者:
class ModelListener implements EventSubscriberInterface
{
public function __construct(VehicleExtractorService $extractor)
{
$this->extractor = $extractor;
}
public static function getSubscribedEvents()
{
return [
FormEvents::PRE_SUBMIT => 'onPreSubmitData',
];
}
public function onPreSubmitData(FormEvent $event)
{
// At this point you get only the scalar values, Model hasn't been transformed yet
$data = $event->getData();
$form = $event->getForm();
$maker_id = $data['maker'];
$model= $form->get('model');
$options = $model->getConfig()->getOptions();
if (!empty($maker_id)) {
unset($options['attr']['disabled']);
$options['choices'] = $this->extractor->getModelsFor($maker_id);
$form->remove('model');
$form->add('model', CarModelsSelectType::class, $options );
}
}
}
}
有点恐慌 - 我正在为复杂搜索生成 Symfony 表单,即映射到实体的数据将仅用于搜索查询构建。
我从 ChoiceType 创建了简单的表单、模型和一些扩展类型,用于通过某种逻辑进行预填充选择。使用 GET 方法提交表单。
例如,在模型中您会找到 maker
和 model
字段。后者在选择制造商后在前端填充 AJAX。当我提交表单时,maker
和 model
具有非默认值,handleRequest
仅填充模型的 maker
属性,但model
留空。如果选中,复选框也会正确填充。总而言之,$form->getData()
returns 只是 Maker 和复选框,其他字段为空。 $request->query
具有所有参数。
数据映射器在这里毫无意义。而且数据中也没有什么可以转换的,模型主要来自标量值。请求包含所有内容,但未正确处理。我尝试实现 ChoiceLoaderInterface
,但这对我不起作用,因为在加载选择期间我必须访问表单的 options
,而我没有(我使用这篇文章 https://speakerdeck.com/heahdude/symfony-forms-use-cases-and-optimization).
我正在使用 Symfony 4.2.4; PHP 7.2.
控制器的方法
/**
* @Route("/search/car", name="car_search", methods={"GET"})
* @param Request $request
*/
public function carSearchAction(Request $request)
{
$carModel = new CarSimpleSearchModel();
$form = $this->createForm(CarSimpleSearchType::class, $carModel);
$form->handleRequest($request);
$form->getData();
.....
}
CarSimpleSearchModel
class CarSimpleSearchModel
{
public $maker;
public $model;
public $priceFrom;
public $priceTo;
public $yearFrom;
public $yearTo;
public $isCompanyOwner;
public $isPrivateOwners;
public $isRoublePrice;
}
CarSimpleSearch键入表格
class CarSimpleSearchType extends AbstractType
{
protected $urlGenerator;
public function __construct(UrlGeneratorInterface $urlGenerator)
{
$this->urlGenerator = $urlGenerator;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('vehicle_type', HiddenType::class, [
'data' => VehicleTypeType::CAR,
'mapped' => false,
])
->add('maker', CarMakerSelectType::class)
->add('model', CarModelsSelectType::class)
->add(
'priceFrom',
VehiclePriceRangeType::class,
[
'vehicle_type' => VehicleTypeType::CAR,
]
)
->add(
'priceTo',
VehiclePriceRangeType::class,
[
'vehicle_type' => VehicleTypeType::CAR,
]
)
->add(
'yearFrom',
VehicleYearRangeType::class,
[
'vehicle_type' => VehicleTypeType::CAR,
]
)
->add(
'yearTo',
VehicleYearRangeType::class,
[
'vehicle_type' => VehicleTypeType::CAR,
]
)
->add('isCompanyOwner', CheckboxType::class)
->add('isPrivateOwners', CheckboxType::class)
->add('isRoublePrice', CheckboxType::class)
->add('submit', SubmitType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'data_class' => CarSimpleSearchModel::class,
'compound' => true,
'method' => 'GET',
'required' => false,
'action' => $this->urlGenerator->generate('car_search'),
]
);
}
public function getBlockPrefix()
{
return 'car_search_form';
}
}
CarMakerSelectType 字段
class CarMakerSelectType extends AbstractType
{
/**
* @var VehicleExtractorService
*/
private $extractor;
/**
* VehicleMakerSelectType constructor.
*
* @param VehicleExtractorService $extractor
*/
public function __construct(VehicleExtractorService $extractor)
{
$this->extractor = $extractor;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'placeholder' => null,
'vehicle_type' => null,
'choices' => $this->getVariants(),
]
);
}
private function getVariants()
{
$makers = $this->extractor->getMakersByVehicleType(VehicleTypeType::CAR);
$choices = [];
foreach ($makers as $maker) {
$choices[$maker['name']] = $maker['id'];
}
return $choices;
}
}
CarModelSelectType 字段
class CarModelsSelectType extends AbstractType
{
private $extractor;
public function __construct(VehicleExtractorService $extractor)
{
$this->extractor = $extractor;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'disabled' => true,
]
);
}
}
VehiclePriceRangeType 字段
class VehiclePriceRangeType extends AbstractType
{
private $extractor;
public function __construct(VehicleExtractorService $extractor)
{
$this->extractor = $extractor;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'vehicle_type' => null,
]
);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
foreach ($this->getRange($options['vehicle_type']) as $value) {
$view->vars['choices'][] = new ChoiceView($value, $value, $value);
}
}
private function getRange(int $vehicleType)
{
return PriceRangeGenerator::generate($this->extractor->getMaxVehiclePrice($vehicleType));
}
}
VehicleYearRangeType 字段
class VehicleYearRangeType extends AbstractType
{
private $extractor;
public function __construct(VehicleExtractorService $extractorService)
{
$this->extractor = $extractorService;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'vehicle_type' => null,
]
);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
foreach ($this->getRange($options['vehicle_type']) as $value) {
$view->vars['choices'][] = new ChoiceView($value, $value, $value);
}
}
protected function getRange(int $vehicleType): array
{
$yearRange = RangeGenerator::generate(
$this->extractor->getMinYear($vehicleType),
$this->extractor->getMaxYear($vehicleType),
1,
true,
true
);
return $yearRange;
}
}
所以,我可以使用来自 Request
的原始数据并手动验证填充模型并发送给进一步处理,但我想这不是正确的方法,我想通过以下方式填充表单框架。我该怎么办?..
在我的例子中,我有一个依赖 EntityType
填充的 ajax 最初是禁用的。由于 choices
其中 null
,它在提交时返回 InvalidValueException
。我必须做的是创建一个 EventListener
并为当前 'main' 字段添加有效的 choices
。基本上就是这样,或多或少适合你的情况。
原始形式:
// Setup Fields
$builder
->add('maker', CarMakerSelectType::class)
->add('model', CarModelsSelectType::class, [
'choices' => [],
// I was setting the disabled on a Event::PRE_SET_DATA if previous field was null
// since I could be loading values from the database but I guess you can do it here
'attr' => ['disabled' => 'disabled'],
]
);
$builder->addEventSubscriber(new ModelListener($this->extractor));
添加回有效选择的事件订阅者:
class ModelListener implements EventSubscriberInterface
{
public function __construct(VehicleExtractorService $extractor)
{
$this->extractor = $extractor;
}
public static function getSubscribedEvents()
{
return [
FormEvents::PRE_SUBMIT => 'onPreSubmitData',
];
}
public function onPreSubmitData(FormEvent $event)
{
// At this point you get only the scalar values, Model hasn't been transformed yet
$data = $event->getData();
$form = $event->getForm();
$maker_id = $data['maker'];
$model= $form->get('model');
$options = $model->getConfig()->getOptions();
if (!empty($maker_id)) {
unset($options['attr']['disabled']);
$options['choices'] = $this->extractor->getModelsFor($maker_id);
$form->remove('model');
$form->add('model', CarModelsSelectType::class, $options );
}
}
}
}