11

環境:

2 つのエンティティがあるとします (Doctrine に正しくマッピングされています)。

  1. Postプロパティ { $id(integer, autoinc), $name(string), $tags(collection of Tag)}を持つ
  2. Tag プロパティ { $id(integer, autoinc), $name(string), $posts(collection of Post)}を持つ

この 2 つの関係はMany-To-Many.

問題:

新しい を作成するときPostに、すぐにタグを追加したい。

既に永続化されていることを追加したい場合は、エンティティ フィールド タイプTagsを作成しますが、問題ありません。

しかし、まったく新しいものも追加したい場合はどうすればよいでしょうTagsか? (既存のタグのいくつかを確認し、新しいタグの名前を入力し、別の新しいタグを追加してから、送信後にすべてを適切にPostエンティティに割り当てます)

    新しい投稿を作成:
     名前: [__________]

    タグを追加する
    | |
    |[x] アルファ
    |[ ] ベータ
    |[x] ガンマ
    | |
    |タグが存在しないため、新規作成:
    | |
    |名前: [__________]
    | |
    |+別の新しいタグを追加

これを行う方法はありますか?Symfony 2 の基本は知っていますが、これに対処する方法がわかりません。また、どこにも答えが見つからないことに驚きました。私にとってはよくある問題のようです。私は何が欠けていますか?

4

2 に答える 2

10

マイ タグ エンティティには、タグ名の一意のフィールドがあります。タグの追加には、新しいフォーム タイプとトランスフォーマーを使用します。

フォームタイプ:

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

トランスフォーマー:

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

config.yml

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

新しいタイプを使用します。

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

「symfony」と「smfony」のような類似性は、オートコンプリート機能で防ぐことができます:

タグコントローラー:

<?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>
于 2013-03-28T13:27:13.403 に答える
2

Select2 のタグ入力を使用して、少し異なるアプローチを取りました。

Select2 タグ入力

クライアント側での重複を防ぎ、きれいに見えるという利点があります。

新しく追加されたエンティティを作成するために、DataTransformer ではなく EventSubscriber を使用しています。

詳細については、私の要点を参照してください。以下は TagType と AddEntityChoiceSubscriber です。

AppBundle/フォーム/タイプ/タグタイプ:

<?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);
    }
}
于 2016-05-17T14:47:27.173 に答える