Немного в панике - генерирую форму 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 и вручную проверить-заполнить модель и отправить на дальнейшую обработку, но я предполагаю, что это неправильный путь, и я хочу заполнить форму фреймворком. Как я могу ?..
Привет, @msg, я так и думал и пытался заполнить поле Model, но безуспешно. Я чувствую, что близок к разгадке. Теперь я смотрю на, возможно, неправильное преобразование, потому что мои варианты выбора генерируются динамически, а Symfony class ChoiceToValueTransformer не может разрешать динамически сгенерированные варианты.
Причина в том, что выбор заполняется не choices или choices_loader. Я не могу этого сделать, потому что мне нужно заполнить варианты позже с помощью Ajax или действительно после того, как остальная конфигурация поля будет установлена. @msg, не могли бы вы указать мне направление, о котором вы говорите, пожалуйста? И да, я отладил обработку запроса, и он выдает исключения, но они неявные и не выбрасываются в браузер или /var/log/dev.log






В моем случае у меня был зависимый 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()
Вы правы насчет экстрактора, я обновил ответ для справки. Можете ли вы получить данные там в случае? Возможно, вы правы в том, что добавление ChoiceView вручную перезаписывает переменные. Но вы все еще можете сделать это на PRE_SET_DATA? Не 100%.
Да, спасибо за обновление. Но как вы думаете, что PRE_SUBMIT не изменил результат заполнения поля модели? Кстати, я буду верен PRE_SET_DATA по цене и году.
УПД. С помощью PRE_SUBMIT я исцелил поля priceFrom и priceTo. Модель была заполнена после отправки. Несмотря на одинаковость полей с yearFrom и yearTo, последние этим событием не управляются.
Наконец-то я попал! Были некоторые логические опечатки и неправильное использование. Я имею в виду, что я значительно упростил форму и то, как она начинается, плюс ваше предложение и альт. Спасибо чувак.
Я предполагаю, что это проблема
PRE_SUBMITСобытие, поскольку вы создали форму с отключеннымCarModelSelectTypeбезchoice, поэтому отправленное значение считается недействительным. Но если это так, я думаю, вы должны увидеть исключение. Если это проблема, вам придется изменить форму, чтобы добавить допустимые варианты дляModelна основе выбораMaker.