Symfony 4. 为什么提交的表单只是部分填充模型?

Symfony 4. Why does submitted Form just partially populate the Model?

有点恐慌 - 我正在为复杂搜索生成 Symfony 表单,即映射到实体的数据将仅用于搜索查询构建。

我从 ChoiceType 创建了简单的表单、模型和一些扩展类型,用于通过某种逻辑进行预填充选择。使用 GET 方法提交表单。

例如,在模型中您会找到 makermodel 字段。后者在选择制造商后在前端填充 AJAX。当我提交表单时,makermodel 具有非默认值,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 );
            }
        }
    }
}