Home > Mobile >  How to generate FormType for many-to-many relation for non-existing items in Symfony 6?
How to generate FormType for many-to-many relation for non-existing items in Symfony 6?

Time:01-10

I have simple many-to-many relation between Article and Tag entities. I want to create a new article using FormType and associate tags with it. But the case is: I want to associate tags that may not exist yet.

    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('title', TextType::class)
            ->add('tags', EntityType::class, [
                'class' => Tag::class,
                'multiple' => true
            ])
        ;
    }

This FormType generates a multi select form for existing tags only. But I want to have a <textarea> field, where users can put existing and not existing tags. Then after form submission, existing tags would be associated with the new article, and not existing tags first would be added and then associated with a new article.

I'm pretty new in Symfony world, so excuse me if my problem is trivial.

CodePudding user response:

First, sorry for my English. I will give you an example based on Embarazada and EtiquetaAspectoEmbarazada entities (Many to many relationship) and the use of enter image description here

CodePudding user response:

You should use several tricks to reach it.

  1. Put to options choices all existing tags but as a relation ArticleHasTag.
  2. Write some logic for deleting orphan entities on submit action.

I can show you how it works.

look at choices option and submit the fragment here.

#[Route(path: '/article/{articleId}', methods: ['GET', 'POST'])]
public function articleEdit(string $articleId, Request $request): Response
{
    $article = $this->entityManager->find(Article::class, $articleId);

    if (!$article) {
        throw new NotFoundHttpException();
    }

    $originalTags = new ArrayCollection($article->getArticleHasTagList()->toArray());

    $formBuilder = $this->createFormBuilder($article, [
        'data_class' => Article::class,
    ]);
    $formBuilder->add('title', TextType::class);
    $formBuilder->add('articleHasTagList', EntityType::class, [
        'class' => ArticleHasTag::class,
        'choice_label' => 'tag.name',
        'choice_value' => 'tag.id',
        'multiple' => true,
        'choices' => (function (Article $article) {
            $articleHasTagList = clone $article->getArticleHasTagList();
            $tags = $this->entityManager->getRepository(Tag::class)->findAll();

            foreach ($tags as $tag) {
                /** @var ArticleHasTag[] $articleHasTagList */
                foreach ($articleHasTagList as $articleHasTag) {
                    if ($tag === $articleHasTag->getTag()) {
                        continue 2;
                    }
                }

                $articleHasTag = new ArticleHasTag();
                $articleHasTag->setArticle($article);
                $articleHasTag->setTag($tag);

                $articleHasTagList->add($articleHasTag);
            }

            return $articleHasTagList;
        })($article),
    ]);
    $formBuilder->add('submit', SubmitType::class);

    $form = $formBuilder->getForm();
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $this->entityManager->persist($article);

        /** @var ArticleHasTag[] $originalTags */
        foreach ($originalTags as $articleHasTag) {
            if (!$article->getArticleHasTagList()->contains($articleHasTag)) {
                $this->entityManager->remove($articleHasTag);
            }
        }

        foreach ($article->getArticleHasTagList() as $articleHasTag) {
            $this->entityManager->persist($articleHasTag);
        }

        $this->entityManager->flush();

        return $this->redirectToRoute('app_app_articleedit', [
            'articleId' => $articleId,
        ]);
    }

    return $this->render('base.html.twig', [
        'form' => $form->createView(),
    ]);
}

of course, you also should make entities.

// src/Entity/Article.php

declare(strict_types=1);

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToMany;

#[Entity]
class Article
{
    #[Id]
    #[Column(type: 'bigint')]
    #[GeneratedValue]
    private string $id;

    #[Column]
    private string $title;

    /**
     * @var Collection<int, ArticleHasTag>
     */
    #[OneToMany(mappedBy: 'article', targetEntity: ArticleHasTag::class, fetch: 'EAGER')]
    private Collection $articleHasTagList;

    public function __construct()
    {
        $this->articleHasTagList = new ArrayCollection();
    }

    public function getId(): string
    {
        return $this->id;
    }

    public function setId(string $id): void
    {
        $this->id = $id;
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function setTitle(string $title): void
    {
        $this->title = $title;
    }

    public function addArticleHasTagList(ArticleHasTag $articleHasTag): void
    {
        $articleHasTag->getTag()->addTagHasArticleList($articleHasTag);
        $articleHasTag->setArticle($this);

        $this->articleHasTagList->add($articleHasTag);
    }

    public function removeArticleHasTagList(ArticleHasTag $articleHasTag): void
    {
        $articleHasTag->getTag()->removeTagHasArticleList($articleHasTag);

        $this->articleHasTagList->removeElement($articleHasTag);
    }

    public function getArticleHasTagList(): Collection
    {
        return $this->articleHasTagList;
    }
}
// src/Entity/ArticleHasTag.php

declare(strict_types=1);

namespace App\Entity;

use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\Index;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Mapping\Table;
use Doctrine\ORM\Mapping\UniqueConstraint;

#[Entity]
#[UniqueConstraint(name: 'uniqArticleIdTagId', columns: ['article_id', 'tag_id'])]
#[Index(columns: ['article_id'], name: 'idxArticleId')]
#[Index(columns: ['tag_id'], name: 'idxTagId')]
class ArticleHasTag
{
    #[Id]
    #[Column(type: 'bigint')]
    #[GeneratedValue]
    private string $id;

    #[ManyToOne(targetEntity: Article::class, fetch: 'EAGER', inversedBy: 'articleHasTagList')]
    #[JoinColumn(nullable: false)]
    private Article $article;

    #[ManyToOne(targetEntity: Tag::class, fetch: 'EAGER', inversedBy: 'tagHasArticleList')]
    #[JoinColumn(nullable: false)]
    private Tag $tag;

    public function getId(): string
    {
        return $this->id;
    }

    public function setId(string $id): void
    {
        $this->id = $id;
    }

    public function getArticle(): Article
    {
        return $this->article;
    }

    public function setArticle(Article $article): void
    {
        $this->article = $article;
    }

    public function getTag(): Tag
    {
        return $this->tag;
    }

    public function setTag(Tag $tag): void
    {
        $this->tag = $tag;
    }
}
// src/Entity/Tag.php

declare(strict_types=1);

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToMany;

#[Entity]
class Tag
{
    #[Id]
    #[Column(type: 'bigint')]
    #[GeneratedValue]
    private string $id;

    #[Column]
    private string $name;

    /**
     * @var Collection<int, ArticleHasTag>
     */
    #[OneToMany(mappedBy: 'tag', targetEntity: ArticleHasTag::class, fetch: 'EAGER')]
    private Collection $tagHasArticleList;

    public function getId(): string
    {
        return $this->id;
    }

    public function setId(string $id): void
    {
        $this->id = $id;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function setName(string $name): void
    {
        $this->name = $name;
    }

    public function __construct()
    {
        $this->tagHasArticleList = new ArrayCollection();
    }

    public function addTagHasArticleList(ArticleHasTag $articleHasTag): void
    {
        if (!$this->tagHasArticleList->contains($articleHasTag)) {
            $this->tagHasArticleList->add($articleHasTag);
        }
    }

    public function removeTagHasArticleList(ArticleHasTag $articleHasTag): void
    {
        if ($this->tagHasArticleList->contains($articleHasTag)) {
            $this->tagHasArticleList->removeElement($articleHasTag);
        }
    }

    public function getTagHasArticleList(): Collection
    {
        return $this->tagHasArticleList;
    }
}
  • Related