Страницы

Поиск по вопросам

пятница, 24 января 2020 г.

Symfony ChoiceType с загрузкой в него Entity из базы с помощью Ajax

#php #ajax #symfony


Задача: форма регистрации, кроме всего прочего есть три выпадающих списка. В первом
выбирается страна, после этого во второй Ajax'ом загружаются регионы и, при выборе
региона, в третий грузятся города. Все три поля грузят данные из трёх моделей: Country,
Region, City.

Код buildForm:

$builder
            ->add('country', EntityType::class, ['class' => 'AppBundle:Country',
                'choice_label' => 'name', 'placeholder' => '--- Выберите страну ---'])
            ->add('region', RegionSelectorType::class, [
                'required' => false])
            ->add('city', EntityType::class, ['class' => 'AppBundle:City', 'choice_label'
=> 'name', 'placeholder' => '--- Выберите город ---', 'required' => false])
            ->add('post_code', null, ['required' => false])
            ->add('address', null, ['required' => false])


Страна грузится сразу из сущности, их там немного. А вот для регионов и городов я
хотел сделать динамическую загрузку, чтобы не грузить зря кучу данных.

Пока застопорился на регионе, город будет по тому же принципу. Проблема в том, что
при отправке формы не происходит трансформация поля, по ID, в сущность. Пробовал и
Using Transformer, Creating a Reusable issue_selector Field, в профилере пишется


  Unable to reverse value for property path "region": The choice "15"
  does not exist or is not unique


Код моего типа RegionSelectorType:

namespace AppBundle\Form;

use AppBundle\Form\DataTransformer\RegionToNumberTransformer;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class RegionSelectorType extends AbstractType
{
    private $manager;

    public function __construct(ObjectManager $manager)
    {
        $this->manager = $manager;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $transformer = new RegionToNumberTransformer($this->manager);
        $builder->addModelTransformer($transformer);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'invalid_message' => 'Выбранный регион не существует',
        ));
    }

    public function getParent()
    {
        return ChoiceType::class;
    }
}


Код трансформера:

namespace AppBundle\Form\DataTransformer;

use AppBundle\Entity\Region;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;

class RegionToNumberTransformer implements DataTransformerInterface
{
    private $manager;

    public function __construct(ObjectManager $manager)
    {
        $this->manager = $manager;
    }

    /**
     * Transforms an object (region) to a string (number).
     *
     * @param  Region|null $region
     * @return string
     */
    public function transform($region)
    {
        if (null === $region) {
            return '';
        }

        return $region->getId();
    }

    /**
     * Transforms a string (number) to an object (region).
     *
     * @param  string $regionNumber
     * @return Region|null
     * @throws TransformationFailedException if object (region) is not found.
     */
    public function reverseTransform($regionNumber)
    {
        // no region number? It's optional, so that's ok
        if (!$regionNumber) {
            return;
        }

        $region = $this->manager
            ->getRepository('AppBundle:Region')
            // query for the issue with this id
            ->find($regionNumber)
        ;

        if (null === $region) {
            // causes a validation error
            // this message is not shown to the user
            // see the invalid_message option
            throw new TransformationFailedException(sprintf(
                'An region with number "%s" does not exist!',
                $regionNumber
            ));
        }

        return $region;
    }
}


Сервис регистрируется так:

services:
    app.form.type.region_selector:
        class: AppBundle\Form\RegionSelectorType
        arguments: ['@doctrine.orm.entity_manager']
        tags:
            - { name: form.type, alias: region_selector }


В общем, все по мануалу. Функция reverseTransform() трансформера вообще не срабатывает.
Стоит только заменить в getParent() ChoiceType::class на TextType::class, как в примере
все работает: reverseTransform() срабатывает, сущность создается. С EntityType вместо
моего типа тоже все работает. Создаю регион типа EntityType, в choices подсовываю пустой
массив, - ошибка. Т.е., в поле формы должен присутствовать EntityType тот, что потом
будет возвращен сабмитом.

Первое что приходит в голову, создавать EventListеner, отлавливать передаваемое ChoiceType'ом
значение и создавать на лету сущность, но зачем тогда DataTransformer'ы?

Symfony использую 3, но, думаю, на второй будут примерно те же проблемы и решение.
    


Ответы

Ответ 1



но зачем тогда DataTransformer'ы? DataTransformerы тут ни при чём. Первое что приходит в голову, создавать EventListеner Как вы сами верно заметили, можно создавать EventListеner. Но не обязательно использовать ChoiceType, можно использовать также EntityType. Ниже примеры кода: Форма регистрации: namespace AppBundle\Form; use AppBundle\Entity\City; use AppBundle\Entity\Country; use AppBundle\Entity\Region; use Doctrine\ORM\EntityRepository; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; class RegistrationForm extends AbstractType { public function buildForm( FormBuilderInterface $builder, array $options ) { $builder ->add('country', EntityType::class, [ 'class' => Country::class, ]) ->add('region', EntityType::class, [ 'class' => Region::class, 'query_builder' => function(EntityRepository $er) { $qb = $er->createQueryBuilder('r') ->where('r.country IS NULL') ; return $qb; }, ]) ->add('city', EntityType::class, [ 'class' => City::class, 'query_builder' => function(EntityRepository $er) { $qb = $er->createQueryBuilder('c') ->where('c.region IS NULL') ; return $qb; }, ]) ; $builder->addEventListener(FormEvents::POST_SET_DATA, function(FormEvent $event) { $data = $event->getData(); $form = $event->getForm(); /** @var $country Country */ $country = $data['country']; $country = $country ? $country->getId() : false; /** @var $region Region */ $region = $data['region']; $region = $region ? $region->getId() : false; $this->modifyForm($form, $country, $region); }); $builder->addEventListener(FormEvents::POST_SUBMIT, function(FormEvent $event) { $data = $event->getData(); $form = $event->getForm(); /** @var $country integer */ $country = (int)$data['country']; $region = (int)$data['region']; $this->modifyForm($form, $country, $region); }); } protected function modifyForm(FormInterface $form, $country, $region = null) { if ( $country ) { $form->add('region', EntityType::class, [ 'class' => Region::class, 'query_builder' => function(EntityRepository $er) use ( $country ) { $qb = $er->createQueryBuilder('r') ->where('r.country = :country') ->setParameter('country', $country) ->orderBy('r.name') ; return $qb; } ]); if ( $region ) { $form->add('city', EntityType::class, [ 'class' => City::class, 'query_builder' => function(EntityRepository $er) use ( $region ) { $qb = $er->createQueryBuilder('c') ->where('c.region = :region') ->setParameter('region', $region) ->orderBy('r.name') ; return $qb; } ]); } } } } По умолчанию, регионы и города будут пустые, добились этого посредством параметра quiery_builder. Если указана страна, то мы ловим это в eventListener, и меняем query_builder, чтобы получить регионы этой страны. Для городов тоже самое. Думаю, с остальными справитесь.

Комментариев нет:

Отправить комментарий