События формы Symfony2 и модельные трансформаторы
Я привязываюсь к узлам, пытаясь бороться с создателями форм, событиями и трансформаторами Symfony2... надеюсь, кто-то здесь более опытен и может помочь!
У меня есть поле формы (выберите раскрывающийся список), который содержит некоторые значения (краткий список), который сопоставляется с Entity. Один из этих вариантов - "другой". Предположим, что AJAX пока нет, и когда пользователь отправляет форму, я хочу определить, выбрали ли они "другое" (или любой другой вариант, не входящий в список). Если они выбрали один из этих вариантов, то должен быть показан полный список опций, иначе просто покажите краткий список. Должно быть легко, не так ли?;)
Итак, у меня есть свой тип формы, и он отображает основной список как раз отлично. Код выглядит примерно так:
namespace Company\ProjectBundle\Form\Type;
use ...
class FancyFormType extends AbstractType {
private $fooRepo;
public function __construct(EntityManager $em, FooRepository $fooRepo)
{
$this->fooRepo = $fooRepo;
}
public function buildForm(FormBuilderInterface $builder, array $options) {
/** @var Bar $bar */
$bar = $builder->getData();
$fooTransformer = new FooToStringTransformer($options['em']);
$builder
->add($builder
->create('linkedFoo', 'choice', array(
'choices' => $this->fooRepo->getListAsArray(
$bar->getLinkedfoo()->getId()
),
))
->addModelTransformer($fooTransformer)
)
;
// ...
}
// ...
}
Теперь я хочу проверить представленное значение, поэтому я использую прослушиватель форм событий следующим образом.
public function buildForm(FormBuilderInterface $builder, array $options) {
// ... This code comes just after the snippet shown above
$builder->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) {
/** @var EntityManager $em */
$em = $event->getForm()->getConfig()->getOption('em');
$data = $event->getData();
if (empty($data['linkedFoo'])) return;
$selectedFoo = $data['linkedfoo'];
$event->getForm()->add('linkedFoo', 'choice', array(
'choices' => $em
->getRepository('CompanyProjectBundle:FooShortlist')
->getListAsArray($selectedFoo)
,
));
//@todo - needs transformer?
});
}
Однако он не работает с сообщением об ошибке:
Notice: Object of class Proxies\__CG__\Company\ProjectBundle\Entity\Foo could not be converted to int in \path\to\project\symfony\symfony\src\Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList.php line 458
Я предполагаю эту ошибку потому, что, когда linkedFoo
был переписан, он удалил modelTransformer
? Я пробовал различные способы доступа к строителю при закрытии события, но это, похоже, не работало (возвращаемые значения были неожиданными). Есть ли другой метод, который я должен использовать в случае, отличном от $event->getForm()->add()
? Или есть более фундаментальная проблема с моим подходом здесь?
В принципе, я не хочу вмешиваться в конфигурацию/трансформаторы/метки полей linkedFoo
, кроме как изменить доступные варианты... есть ли другой способ сделать это? Например. что-то вроде $form->getField()->updateChoices()
?
Заранее благодарим за любую помощь, которую вы можете предложить!
С
P.S. есть ли более хорошая документация или обсуждение форм, событий и т.д., чем на веб-сайте Symfony? Например. какая разница между PRE_SET_DATA, PRE_SUBMIT, SUBMIT и т.д.? Когда они увольняются? Для чего они должны использоваться? Как наследование работает с полями пользовательских форм? Что такое форма и строитель, как они взаимодействуют, и когда вы должны иметь дело с каждым? Как, когда и почему вы должны использовать FormFactory, вы можете получить доступ через $form->getConfig()->getFormFactory()
? Etc..
Изменить: В ответ на предложение Флориана здесь появилась дополнительная информация о вещах, которые были опробованы, но не работают:
Если вы попытаетесь получить FormBuilder внутри события следующим образом:
/** @var FormBuilder $builder */
$builder = $event->getForm()->get('linkedFoo')->getConfig();
$event->getForm()->add($builder
->create('linkedFoo', 'choice', array(
'choices' => $newChoices,
'label' =>'label',
))
->addModelTransformer(new FooToStringTransformer($em))
);
Затем вы получите сообщение об ошибке:
FormBuilder methods cannot be accessed anymore once the builder is turned
into a FormConfigInterface instance.
Итак, вы попробуете что-то вроде того, что предложил Флориан, т.е.
$event->getForm()->add('linkedFoo', 'choice', array(
'choices' => $newChoices,
));
$event->getForm()->get('linkedFoo')->getConfig()->addModelTransformer(new FooToStringTransformer($em));
... но вместо этого вы получите эту ошибку:
Notice: Object of class Proxies\__CG__\Company\ProjectBundle\Entity\Foo could not be converted to int
in C:\path\to\vendor\symfony\symfony\src\Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList.php line 458
Кажется, что вторая строка (которая добавляет ModelTransformer) никогда не вызывается, потому что вызов ->add()
терпит неудачу, прежде чем вы сможете туда добраться.
Ответы
Ответ 1
Благодаря идеям sstok (на github), я думаю, что теперь у меня это работает. Ключом является создание настраиваемого типа формы, а затем его использование для добавления ModelTransformer.
Создайте собственный тип формы:
namespace Caponica\MagnetBundle\Form\Type;
use ...
class FooShortlistChoiceType extends AbstractType {
protected $em;
public function __construct(EntityManager $entityManager)
{
$this->em = $entityManager;
}
public function buildForm(FormBuilderInterface $builder, array $options) {
$fooTransformer = new FooToStringTransformer($this->em);
$builder
->addModelTransformer($fooTransformer)
;
}
public function getParent() {
return 'choice';
}
public function getName() {
return 'fooShortlist';
}
}
Создайте определение службы для нового типа:
company_project.form.type.foo_shortlist:
class: Company\ProjectBundle\Form\Type\FooShortlistChoiceType
tags:
- { name: form.type, alias: fooShortlist }
arguments:
- @doctrine.orm.entity_manager
Теперь код основной формы выглядит примерно так:
namespace Company\ProjectBundle\Form\Type;
use ...
class FancyFormType extends AbstractType {
private $fooRepo;
public function __construct(FooRepository $fooRepo)
{
$this->fooRepo = $fooRepo;
}
public function buildForm(FormBuilderInterface $builder, array $options) {
/** @var Bar $bar */
$bar = $builder->getData();
$fooTransformer = new FooToStringTransformer($options['em']);
$builder
->add('linkedFoo', 'fooShortlist', array(
'choices' => $this->fooRepo->getListAsArray(
$bar->getLinkedfoo()->getId()
),
))
;
$builder->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) {
/** @var EntityManager $em */
$em = $event->getForm()->getConfig()->getOption('em');
$data = $event->getData();
if (empty($data['linkedFoo'])) return;
$selectedFoo = $data['linkedFoo'];
$event->getForm()->add('linkedFoo', 'fooShortlist', array(
'choices' => $em->getRepository('CaponicaMagnetBundle:FooShortlist')->getListAsArray($selectedFoo),
'label' => 'label'
));
});
// ...
}
// ...
}
Ключ состоит в том, что этот метод позволяет встраивать ModelTransformer в пользовательский тип поля, так что всякий раз, когда вы добавляете новый экземпляр этого типа, он автоматически добавляет ModelTransformer для вас и предотвращает предыдущий цикл "не может добавить поле без трансформатора И не может добавить трансформатор без поля"
Ответ 2
Ваш слушатель выглядит (почти:)) нормально.
Просто используйте PRE_SUBMIT.
В этом случае $event->getData()
будет представлять собой необработанные данные формы (массив), которые отправляются.
$selectedFoo
будет содержать "другое".
Если это так, вы замените поле "короткий" "выбор" полным, используя formFactory в слушателе.
$builder->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) {
$data = $event->getData();
if (empty($data['linkedFoo']) || $data['linkedFoo'] !== 'other') {
return;
}
// now we know user choose "other"
// so we'll change the "linkedFoo" field with a "fulllist"
$event->getForm()->add('linkedFoo', 'choice', array(
'choices' => $fullList, // $em->getRepository('Foo')->getFullList() ?
));
$event->getForm()->get('linkedFoo')->getConfig()->addModelTransformer(new FooTransformer);
});
Ты задал столько вопросов, я не знаю, с чего начать.
Относительно данныхТрансформаторы:
пока вы не захотите преобразовать необработанные данные в другое представление ( "2013-01-01" → новое DateTime ( "2013-01-01" )), тогда вам не нужны трансформаторы.