Symfony2 и Selectize.js: Самый простой способ сохранить новые элементы в типе поля объекта?

В Symfony2 у меня есть BandType, где я добавляю объект Tag:

->add('tags', 'entity', [
     'label' => 'Tags',
     'class' => 'DbBundle:Tag',
     'property' => 'title',
     'multiple'  =>  true,
])

Это создает несколько элементов select, где я могу выбрать существующие теги из базы данных (Doctrine). Но мне нужно добавить новые динамические теги, которые еще не существуют.

На стороне клиента я использую плагин jQuery Selectize.js, что позволяет мне добавлять новый тег в поле выбора. Но после отправки формы новые теги не сохраняются.

Итак, мой вопрос - , что является самым ясным способом сохранения новых элементов из окна выбора (тип поля сущности)?

Ответ 1

Используйте Data Transformer для своего объекта. И в методе reverseTransform, если вы не найдете новую добавленную группу, просто создайте ее там вместо того, чтобы бросать TransformationFailedException.

Ответ 2

Одним из возможных решений является использование FormEvents. Вот пример кода:

namespace AppBundle\Form;

use AppBundle\Entity\Tag;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\OptionsResolver\OptionsResolver;

class PostType extends AbstractType
{
    /**
     * @var ObjectManager
     */
    private $manager;

    /**
     * Constructor
     *
     * @param ObjectManager $manager
     */
    public function __construct(ObjectManager $manager)
    {
        $this->manager = $manager;
    }

    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title')
            ->add('content')
            ->add('tags')
        ;
        $builder->get('tags')->addEventListener(
            FormEvents::PRE_SUBMIT,
            function (FormEvent $event) {
                $choiceList = $event->getForm()->getConfig()->getAttribute('choice_list');
                $array = is_null($event->getData()) ? [] : $event->getData();
                $choices = $choiceList->getChoicesForValues($array);

                if (count($choices) !== count($array)) {
                    $values = $choiceList->getValuesForChoices($choices);
                    $diff = array_merge(array_diff($values, $array), array_diff($array, $values));

                    foreach ($diff as $value) {
                        $new = new Tag($value);
                        $this->manager->persist($new);
                        $this->manager->flush();
                        $values[] = $new->getId();
                    }

                    $event->setData($values);
                }
            }
        );
    }

    /**
     * @param OptionsResolver $resolver
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Post'
        ));
    }
}

Ответ 3

Как описано в другом ответе, вы захотите использовать Data Transformer для своей сущности и вернуть новый объект, если вы 't найдите тот, который пользователь попросил.

Есть несколько способов сделать это. Это один из способов сделать это, упрощенное из приложения, которое просто использует selectize.js, но эти концепции применимы к любому из них, которые могут быть у вас на вашем интерфейсе.

class SubjectTransformer implements DataTransformerInterface
{
    protected $em;

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

    //public function transform($val) { ... }

    public function reverseTransform($str)
    {
        $repo = $this->em->getRepository('AppBundle:Subject');

        $subject = $repo->findOneByName($str);
        if($subject)
            return $subject;

        //Didn't find it, so it must be new 
        $subject = new Subject;
        $subject->setName($str);
        $this->em->persist($subject);

        return $subject;
    }
}

В частности, это DataTransformer для entry_type поля CollectionType:

  • принимает менеджер объектов в своем конструкторе
  • в reverseTransform, использует EM для извлечения значения из базы данных
  • Если он не находит его, он создает новый объект и сохраняет его
  • явно не очищает объект, если ваш процессор/контроллер формы хочет выполнить дополнительную проверку на новом объекте до фактического его совершения.

Другие возможные варианты включают не вызов em->persist; вызов em->flush; или (возможно, идеально) передачу службы для управления поиском/созданием, а не напрямую с помощью диспетчера сущностей. (Такая служба может реализовать почти дублированное обнаружение, фильтрацию на плохом языке, только дать определенным пользователям возможность создавать новые теги и т.д.).