Symfony2 and Selectize.js: Clearest way to persist new items in entity field type?
Asked Answered
S

3

12

In Symfony2 I have BandType, where I add the entity Tag:

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

This generate multiple select element, where I can choose existing tags from database (Doctrine). But I need to add new tags dynamicaly, which don't exist yet.

On a client side I use jQuery plugin Selectize.js, which allows me to add new tag to select box. But after submit form the new tags are not saved.

So my question is - what is the clearest way to persist new items from select box (entity field type)?

Sauder answered 23/4, 2015 at 16:17 Comment(0)
R
11

Use a Data Transformer for your entity. And in the reverseTransform method, if you don't find the newly added band, simply create it there instead of throwing a TransformationFailedException.

Rawson answered 24/4, 2015 at 9:45 Comment(1)
Hi, would you be so kind to post your final solution? ThanksPalmira
H
0

One possible solution is to use FormEvents. Here is example code:

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'
        ));
    }
}
Halvorsen answered 29/1, 2016 at 14:14 Comment(0)
U
-1

As described in the other answer, you'll want to use a Data Transformer for your entity, and return a new entity if you don't find the one the user has asked for.

There are any number of ways you could to this. This is one way to do it, simplified from an application that just happens to use selectize.js, but the concepts apply to anyUI you may have on your front-end.

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;
    }
}

Specifically, this DataTransformer for the entry_type of a CollectionType field:

  • takes an entity manager in its constructor
  • in reverseTransform, uses the EM to retrieve a value from the database
  • If it doesn't find one, it creates a new entity, and persists it
  • explicitly does not flush the entity, in case your form processor/controller wants to perform additional validation on the new entity before actually committing it

Other possible variations include not calling em->persist; calling em->flush; or (probably ideally) passing a service to manage search/creation, rather than using the entity manager directly. (Such a service might implement near-duplicate detection, bad-language filtering, only give certain users the ability to create new tags, etc.)

Unnecessarily answered 29/1, 2016 at 1:50 Comment(4)
Your answer is not about ManyToMany relationship.Halvorsen
@Halvorsen The original question or your bounty doesn't specifically mention ManyToMany relationships, but, this code is used for one in my actual application.Unnecessarily
It's not explicitly written, but yes, it is – and also your solution not works for that case → Each Band (entity) has multiple Tags (Collection)Halvorsen
Ok, the confusion here, then, is that this is not the transformer to a collection. It's the transformer for the sub-form that makes up the collection.Unnecessarily

© 2022 - 2024 — McMap. All rights reserved.