Подтвердить что ты не робот

События формы 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() терпит неудачу, прежде чем вы сможете туда добраться.

4b9b3361

Ответ 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" )), тогда вам не нужны трансформаторы.