#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, чтобы получить регионы этой страны. Для городов тоже самое. Думаю, с остальными справитесь.
Комментариев нет:
Отправить комментарий