Symfony 4. Почему отправленная форма лишь частично заполняет модель?

Немного в панике - генерирую форму Symfony для сложного поиска, т.е. сопоставленные данные с сущностью будут использоваться только для построения поискового запроса.

Я создаю простую форму, модель, некоторые расширенные типы из ChoiceType для выбора предварительного заполнения по некоторой логике. Форма отправляется методом GET.

Например, в модели вы найдете поля maker и model. Последний заполняется во внешнем интерфейсе с помощью AJAX после выбора производителя. Когда я отправляю форму, а maker и model имеют значение, отличное от значения по умолчанию, handleRequest заполняет только свойство maker модели, но model остается пустым. Также флажки правильно заполняются, если они отмечены. В общем, $form->getData() возвращает только 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();

        .....
    }

АвтомобильПростойПоискМодель

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 и вручную проверить-заполнить модель и отправить на дальнейшую обработку, но я предполагаю, что это неправильный путь, и я хочу заполнить форму фреймворком. Как я могу ?..

Я предполагаю, что это проблема PRE_SUBMIT Событие, поскольку вы создали форму с отключенным CarModelSelectType без choice, поэтому отправленное значение считается недействительным. Но если это так, я думаю, вы должны увидеть исключение. Если это проблема, вам придется изменить форму, чтобы добавить допустимые варианты для Model на основе выбора Maker.

msg 18.06.2019 00:09

Привет, @msg, я так и думал и пытался заполнить поле Model, но безуспешно. Я чувствую, что близок к разгадке. Теперь я смотрю на, возможно, неправильное преобразование, потому что мои варианты выбора генерируются динамически, а Symfony class ChoiceToValueTransformer не может разрешать динамически сгенерированные варианты.

Paul Burilichev 18.06.2019 00:23

Причина в том, что выбор заполняется не choices или choices_loader. Я не могу этого сделать, потому что мне нужно заполнить варианты позже с помощью Ajax или действительно после того, как остальная конфигурация поля будет установлена. @msg, не могли бы вы указать мне направление, о котором вы говорите, пожалуйста? И да, я отладил обработку запроса, и он выдает исключения, но они неявные и не выбрасываются в браузер или /var/log/dev.log

Paul Burilichev 18.06.2019 11:38
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Symfony Station Communiqué - 7 июля 2023 г
Symfony Station Communiqué - 7 июля 2023 г
Это коммюнике первоначально появилось на Symfony Station .
Оживление вашего приложения Laravel: Понимание режима обслуживания
Оживление вашего приложения Laravel: Понимание режима обслуживания
Здравствуйте, разработчики! В сегодняшней статье мы рассмотрим важный аспект управления приложениями, который часто упускается из виду в суете...
Установка и настройка Nginx и PHP на Ubuntu-сервере
Установка и настройка Nginx и PHP на Ubuntu-сервере
В этот раз я сделаю руководство по установке и настройке nginx и php на Ubuntu OS.
Коллекции в Laravel более простым способом
Коллекции в Laravel более простым способом
Привет, читатели, сегодня мы узнаем о коллекциях. В Laravel коллекции - это способ манипулировать массивами и играть с массивами данных. Благодаря...
Как установить PHP на Mac
Как установить PHP на Mac
PHP - это популярный язык программирования, который используется для разработки веб-приложений. Если вы используете Mac и хотите разрабатывать...
1
3
819
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В моем случае у меня был зависимый EntityType, заполненный ajax, который изначально отключен. Так как choices где null, он возвращал InvalidValueException при отправке. Что мне нужно было сделать, так это создать EventListener и добавить действительный 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 );
            }
        }
    }
}

Привет @msg У меня есть идея. И пробовал - безуспешно. Просто обратите внимание, что мы должны явно передать $extractor подписчику, так как он создан с помощью new. Да, он перешел к подписчику, и форма была обновлена, но CarSimpleSearchModel не заполнен model id. И причина неработающих вариантов цены и года выглядит так, что мне нужно заполнить поля на основе $options, переданных полям во время построения формы, но не после этого, например buildView() или finishView()

Paul Burilichev 18.06.2019 19:26

Вы правы насчет экстрактора, я обновил ответ для справки. Можете ли вы получить данные там в случае? Возможно, вы правы в том, что добавление ChoiceView вручную перезаписывает переменные. Но вы все еще можете сделать это на PRE_SET_DATA? Не 100%.

msg 18.06.2019 21:14

Да, спасибо за обновление. Но как вы думаете, что PRE_SUBMIT не изменил результат заполнения поля модели? Кстати, я буду верен PRE_SET_DATA по цене и году.

Paul Burilichev 18.06.2019 21:56

УПД. С помощью PRE_SUBMIT я исцелил поля priceFrom и priceTo. Модель была заполнена после отправки. Несмотря на одинаковость полей с yearFrom и yearTo, последние этим событием не управляются.

Paul Burilichev 18.06.2019 22:06

Наконец-то я попал! Были некоторые логические опечатки и неправильное использование. Я имею в виду, что я значительно упростил форму и то, как она начинается, плюс ваше предложение и альт. Спасибо чувак.

Paul Burilichev 19.06.2019 10:30

Другие вопросы по теме