Symfony 2 Entity field type with select and/or add new

后端 未结 2 576
孤独总比滥情好
孤独总比滥情好 2021-02-01 09:48

Context:

Let there be two entities (correctly mapped for Doctrine).

  1. Post with properties {$id (integer, autoinc), $name
相关标签:
2条回答
  • 2021-02-01 10:23

    My Tag entity has a unique field for the tag name. For add Tags I use a new form type and a transformer.

    The Form Type:

    namespace Sg\RecipeBundle\Form\Type;
    
    use Symfony\Component\Form\AbstractType;
    use Symfony\Component\Form\FormBuilderInterface;
    use Symfony\Bridge\Doctrine\RegistryInterface;
    use Symfony\Component\Security\Core\SecurityContextInterface;
    use Sg\RecipeBundle\Form\DataTransformer\TagsDataTransformer;
    
    class TagType extends AbstractType
    {
        /**
         * @var RegistryInterface
         */
        private $registry;
    
        /**
         * @var SecurityContextInterface
         */
        private $securityContext;
    
    
        /**
         * Ctor.
         *
         * @param RegistryInterface        $registry        A RegistryInterface instance
         * @param SecurityContextInterface $securityContext A SecurityContextInterface instance
         */
        public function __construct(RegistryInterface $registry, SecurityContextInterface $securityContext)
        {
            $this->registry = $registry;
            $this->securityContext = $securityContext;
        }
    
        /**
         * {@inheritdoc}
         */
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $builder->addViewTransformer(
                new TagsDataTransformer(
                    $this->registry,
                    $this->securityContext
                ),
                true
            );
        }
    
        /**
         * {@inheritdoc}
         */
        public function getParent()
        {
            return 'text';
        }
    
        /**
         * {@inheritdoc}
         */
        public function getName()
        {
            return 'tag';
        }
    }
    

    The Transformer:

    <?php
    
    /*
     * Stepan Tanasiychuk is the author of the original implementation
     * see: https://github.com/stfalcon/BlogBundle/blob/master/Bridge/Doctrine/Form/DataTransformer/EntitiesToStringTransformer.php
     */
    
    namespace Sg\RecipeBundle\Form\DataTransformer;
    
    use Symfony\Component\Form\DataTransformerInterface;
    use Symfony\Component\Security\Core\SecurityContextInterface;
    use Symfony\Component\Security\Core\Exception\AccessDeniedException;
    use Symfony\Component\Form\Exception\UnexpectedTypeException;
    use Symfony\Bridge\Doctrine\RegistryInterface;
    use Doctrine\ORM\EntityManager;
    use Doctrine\Common\Collections\Collection;
    use Doctrine\Common\Collections\ArrayCollection;
    use Sg\RecipeBundle\Entity\Tag;
    
    /**
     * Tags DataTransformer.
     */
    class TagsDataTransformer implements DataTransformerInterface
    {
        /**
         * @var EntityManager
         */
        private $em;
    
        /**
         * @var SecurityContextInterface
         */
        private $securityContext;
    
    
        /**
         * Ctor.
         *
         * @param RegistryInterface        $registry        A RegistryInterface instance
         * @param SecurityContextInterface $securityContext A SecurityContextInterface instance
         */
        public function __construct(RegistryInterface $registry, SecurityContextInterface $securityContext)
        {
            $this->em = $registry->getEntityManager();
            $this->securityContext = $securityContext;
        }
    
        /**
         * Convert string of tags to array.
         *
         * @param string $string
         *
         * @return array
         */
        private function stringToArray($string)
        {
            $tags = explode(',', $string);
    
            // strip whitespaces from beginning and end of a tag text
            foreach ($tags as &$text) {
                $text = trim($text);
            }
    
            // removes duplicates
            return array_unique($tags);
        }
    
        /**
         * Transforms tags entities into string (separated by comma).
         *
         * @param Collection | null $tagCollection A collection of entities or NULL
         *
         * @return string | null An string of tags or NULL
         * @throws UnexpectedTypeException
         */
        public function transform($tagCollection)
        {
            if (null === $tagCollection) {
                return null;
            }
    
            if (!($tagCollection instanceof Collection)) {
                throw new UnexpectedTypeException($tagCollection, 'Doctrine\Common\Collections\Collection');
            }
    
            $tags = array();
    
            /**
             * @var \Sg\RecipeBundle\Entity\Tag $tag
             */
            foreach ($tagCollection as $tag) {
                array_push($tags, $tag->getName());
            }
    
            return implode(', ', $tags);
        }
    
        /**
         * Transforms string into tags entities.
         *
         * @param string | null $data Input string data
         *
         * @return Collection | null
         * @throws UnexpectedTypeException
         * @throws AccessDeniedException
         */
        public function reverseTransform($data)
        {
            if (!$this->securityContext->isGranted('ROLE_AUTHOR')) {
                throw new AccessDeniedException('Für das Speichern von Tags ist die Autorenrolle notwendig.');
            }
    
            $tagCollection = new ArrayCollection();
    
            if ('' === $data || null === $data) {
                return $tagCollection;
            }
    
            if (!is_string($data)) {
                throw new UnexpectedTypeException($data, 'string');
            }
    
            foreach ($this->stringToArray($data) as $name) {
    
                $tag = $this->em->getRepository('SgRecipeBundle:Tag')
                    ->findOneBy(array('name' => $name));
    
                if (null === $tag) {
                    $tag = new Tag();
                    $tag->setName($name);
    
                    $this->em->persist($tag);
                }
    
                $tagCollection->add($tag);
    
            }
    
            return $tagCollection;
        }
    }
    

    The config.yml

    recipe.tags.type:
        class: Sg\RecipeBundle\Form\Type\TagType
        arguments: [@doctrine, @security.context]
        tags:
            - { name: form.type, alias: tag }
    

    use the new Type:

            ->add('tags', 'tag', array(
                'label' => 'Tags',
                'required' => false
                ))
    

    Similarities, like "symfony" and "smfony" can be prevented with an autocomplete function:

    TagController:

    <?php
    
    namespace Sg\RecipeBundle\Controller;
    
    use Symfony\Bundle\FrameworkBundle\Controller\Controller;
    use Symfony\Component\HttpFoundation\Response;
    use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
    use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
    
    /**
     * Tag controller.
     *
     * @Route("/tag")
     */
    class TagController extends Controller
    {
        /**
         * Get all Tag entities.
         *
         * @Route("/tags", name="tag_tags")
         * @Method("GET")
         *
         * @return \Symfony\Component\HttpFoundation\Response
         */
        public function getTagsAction()
        {
            $request = $this->getRequest();
            $isAjax = $request->isXmlHttpRequest();
    
            if ($isAjax) {
                $em = $this->getDoctrine()->getManager();
    
                $search = $request->query->get('term');
    
                /**
                 * @var \Sg\RecipeBundle\Entity\Repositories\TagRepository $repository
                 */
                $repository = $em->getRepository('SgRecipeBundle:Tag');
    
                $qb = $repository->createQueryBuilder('t');
                $qb->select('t.name');
                $qb->add('where', $qb->expr()->like('t.name', ':search'));
                $qb->setMaxResults(5);
                $qb->orderBy('t.name', 'ASC');
                $qb->setParameter('search', '%' . $search . '%');
    
                $results = $qb->getQuery()->getScalarResult();
    
                $json = array();
                foreach ($results as $member) {
                    $json[] = $member['name'];
                };
    
                return new Response(json_encode($json));
            }
    
            return new Response('This is not ajax.', 400);
        }
    }
    

    form.html.twig:

    <script type="text/javascript">
    
        $(document).ready(function() {
    
            function split(val) {
                return val.split( /,\s*/ );
            }
    
            function extractLast(term) {
                return split(term).pop();
            }
    
            $("#sg_recipebundle_recipetype_tags").autocomplete({
                source: function( request, response ) {
                    $.getJSON( "{{ path('tag_tags') }}", {
                        term: extractLast( request.term )
                    }, response );
                },
                search: function() {
                    // custom minLength
                    var term = extractLast( this.value );
                    if ( term.length < 2 ) {
                        return false;
                    }
                },
                focus: function() {
                    // prevent value inserted on focus
                    return false;
                },
                select: function( event, ui ) {
                    var terms = split( this.value );
                    // remove the current input
                    terms.pop();
                    // add the selected item
                    terms.push( ui.item.value );
                    // add placeholder to get the comma-and-space at the end
                    terms.push( "" );
                    this.value = terms.join( ", " );
                    return false;
                }
            });
    
        });
    
    </script>
    
    0 讨论(0)
  • 2021-02-01 10:37

    I took a slightly different approach using Select2's tag input:

    It has the advantage that it prevents duplicates on the client side and looks pretty.

    To create the newly added entities, I am using a EventSubscriber rather than a DataTransformer.

    For a few more details, see my gist. Below are the TagType and the AddEntityChoiceSubscriber.

    AppBundle/Form/Type/TagType:

    <?php
    
    namespace AppBundle\Form\Type;
    
    use Symfony\Component\Form\AbstractType;
    use Symfony\Component\Form\FormBuilderInterface;
    use AppBundle\Form\EventListener\AddEntityChoiceSubscriber;
    use Symfony\Bridge\Doctrine\Form\Type\EntityType;
    
    class TagType extends AbstractType
    {
        /**
         * {@inheritdoc}
         */
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $subscriber = new AddEntityChoiceSubscriber($options['em'], $options['class']);
            $builder->addEventSubscriber($subscriber);
        }
    
        /**
         * {@inheritdoc}
         */
        public function getParent()
        {
            return EntityType::class;
        }
    
        /**
         * {@inheritdoc}
         */
        public function getName()
        {
            return 'tag';
        }
    }
    

    AppBundle/Form/EventListener/AddEntityChoiceSubscriber:

    <?php
    
    namespace TriprHqBundle\Form\EventListener;
    
    use Symfony\Component\EventDispatcher\EventSubscriberInterface;
    use Doctrine\ORM\EntityManager;
    use Symfony\Component\Form\FormEvents;
    use Symfony\Component\Form\FormEvent;
    
    class AddEntityChoiceSubscriber implements EventSubscriberInterface
    {
        /**
         * @var EntityManager
         */
        protected $em;
    
        /**
         * The name of the entity
         *
         * @var string
         */
        protected $entityName;
    
        public function __construct(EntityManager $em, string $entityName)
        {
            $this->em = $em;
            $this->entityName = $entityName;
        }
    
        public static function getSubscribedEvents()
        {
            return [
                FormEvents::PRE_SUBMIT => 'preSubmit',
            ];
        }
    
        public function preSubmit(FormEvent $event)
        {
            $data = $event->getData();
    
            if (!is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) {
                $data = [];
            }
    
            // loop through all values
            $repository = $this->em->getRepository($this->entityName);
            $choices = array_map('strval', $repository->findAll());
            $className = $repository->getClassName();
            $newChoices = [];
            foreach($data as $key => $choice) {
                // if it's numeric we consider it the primary key of an existing choice
                if(is_numeric($choice) || in_array($choice, $choices)) {
                    continue;
                }
                $entity = new $className($choice);
                $newChoices[] = $entity;
                $this->em->persist($entity);
            }
            $this->em->flush();
    
            // now we need to replace the text values with their new primary key
            // otherwise, the newly added choice won't be marked as selected
            foreach($newChoices as $newChoice) {
                $key = array_search($newChoice->__toString(), $data);
                $data[$key] = $newChoice->getId();
            }
    
            $event->setData($data);
        }
    }
    
    0 讨论(0)
提交回复
热议问题