skip to Main Content

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.

2

Answers


  1. 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 tetranz /select2entity-bundle The idea of that is register an pregnant woman and asociate her many tags, in case of entered text a tag name that not exist, it could be inserted from the Embarazada form type.

    EmbarazadaType form class sumarized:

    namespace AppForm;
    
    use AppEntityEtiquetaAspectoEmbarazada;
    use DoctrinePersistenceManagerRegistry;
    use SymfonyBridgeDoctrineFormTypeEntityType;
    use SymfonyComponentFormAbstractType;
    use SymfonyComponentFormExtensionCoreTypeTextType;
    use SymfonyComponentFormFormBuilderInterface;
    use SymfonyComponentOptionsResolverOptionsResolver;
    use TetranzSelect2EntityBundleFormTypeSelect2EntityType;
    
    class EmbarazadaType extends AbstractType
    {
    
        public function __construct(private readonly ManagerRegistry $em, private readonly array $formParams)
        {
    
        }
    
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $builder
                    ->add('nombre', TextType::class, [
                        'label' => 'Nombre y Apellidos*',
                    ])
    
                    ->add('otrosAspectos', Select2EntityType::class, [
                        'label' => 'Aspectos de interés',
                        'label_attr' => ['class' => 'text-secondary'],
                        'class' => EtiquetaAspectoEmbarazada::class,
                        'remote_route' => 'embarazada_encontrarEtiquetaAspectos', // an action to search tags by text typed.
                        'primary_key' => 'id',
                        'text_property' => 'text',
                        'multiple' => true,
                        'allow_clear' => false,
                        'delay' => 250,
                        'cache' => false,
                        'minimum_input_length' => 3,
                        'scroll' => true,
                        'page_limit' => $this->formParams['limite_resultados_etiquetas'],
                        'language' => 'es',
                        'width' => '100%',
                        'placeholder' => '',
                        'allow_add' => [
                            'enabled' => true,
                            'new_tag_text' => '(NUEVA)',
                            'new_tag_prefix' => '**',
                            'tag_separators' => '[",", ";", " "]'
                        ],
            ]);
    
        }
    
        public function configureOptions(OptionsResolver $resolver)
        {
            $resolver->setDefaults([
                'data_class' => Embarazada::class,
                'attr' => ['id' => 'embarazadaForm', 'autocomplete' => 'off'],
            ]);
        }
    
    }
    

    The logic of the action captarAction (register pregnam woman) look for the comments inside:

    public function captarAction(Request $request, ManagerRegistry $manager, UuidEncoder $uuidEncoder, LoggerInterface $logger): Response
        {
            if ($request->isXmlHttpRequest()) {
                try {               
    
                    $nuevaEmbarazada = new Embarazada();                
    
                    $form = $this->createForm(EmbarazadaType::class, $nuevaEmbarazada, [
                        'action' => $this->generateUrl('embarazadas_captar', ['cmfId' => $uuidEncoder->encode($estructuraOrganizativa->getIdPublico())]),
                        'method' => Request::METHOD_POST,
                    ]);
    
                    if ($request->isMethod(Request::METHOD_POST)) {
                        $form->handleRequest($request);
    
                        if ($form->isSubmitted() && $form->isValid()) {
    
                            
                            $conn = $em->getConnection();
                            $conn->beginTransaction();
                            try {
                                
                                /**
                                 * All tags present in the collection otrosAspectos with empty id, are takens as news, so need to remove '**' that indicate
                                 * in that case the end of a new tag and persist, checking first if not exist in database another tag with similar name without '**'.
                                **/
    
                                foreach ($nuevaEmbarazada->getOtrosAspectos() as $etiqueta) {
                                    if (empty($etiqueta->getId())) {
                                        $etiquetaSimilar = $manager->getRepository(EtiquetaAspectoEmbarazada::class)
                                                ->findOneBy(['text' => trim((string) $etiqueta->getText(), "/*+/")]);
    
                                        // find similar tag without '**' symbol, if exist replace in the associated collection.
                                        if (!is_null($etiquetaSimilar)) {
                                            $nuevaEmbarazada->getOtrosAspectos()->removeElement($etiqueta); // remove the original tag with '**' and insert the new one without them.
                                            $nuevaEmbarazada->addOtrosAspecto($etiquetaSimilar);
                                        } else {
                                            // if not exist, persist a new tag without '**' symbol.
                                            $etiqueta->setText(trim((string) $etiqueta->getText(), "/*+/"));
                                            $em->persist($etiqueta);
                                        }
                                    } else {
                                        continue;
                                    }
                                }
                                
    
                                $em->persist($nuevaEmbarazada);
                                $em->flush();
                                $conn->commit();
                            } catch (Exception $exc) {
                                $conn->rollback();
                                $conn->close();
    
                                // return user friendly errors
                            }
    
                            return new Response("La embarazada fue registrada satisfactoriamente.");
                        } else {
                            $form->addError(new FormError("Error al registrar la embarazada. Rectifique los errores señalados en cada sección."));
                            return new Response($this->renderView('Embarazadas/frmCaptarEmbarazada.html.twig', ['form' => $form->createView(), 'enEdicion' => false, 'edadGestacionalActual' => $nuevaEmbarazada->getEdadGestacional()]), Response::HTTP_NOT_ACCEPTABLE);
                        }
                    }
    
                    return $this->render('Embarazadas/frmCaptarEmbarazada.html.twig', ['form' => $form->createView(), 'enEdicion' => false, 'edadGestacionalActual' => $nuevaEmbarazada->getEdadGestacional()]);
                } catch (Exception $exc) {
                    $logger->error(sprintf("[%s:%s]: %s", self::class, __FUNCTION__, $exc->getMessage()));
                    return new Response("Ocurrió un error inesperado al ejecutar la operación", Response::HTTP_INTERNAL_SERVER_ERROR);
                }
            } else {
                throw $this->createNotFoundException("Recurso no encontrado");
            }
        }
    

    Check the final result (red frame):
    enter image description here

    Login or Signup to reply.
  2. 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 AppEntity;
    
    use DoctrineCommonCollectionsArrayCollection;
    use DoctrineCommonCollectionsCollection;
    use DoctrineORMMappingColumn;
    use DoctrineORMMappingEntity;
    use DoctrineORMMappingGeneratedValue;
    use DoctrineORMMappingId;
    use DoctrineORMMappingOneToMany;
    
    #[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 AppEntity;
    
    use DoctrineORMMappingColumn;
    use DoctrineORMMappingEntity;
    use DoctrineORMMappingGeneratedValue;
    use DoctrineORMMappingId;
    use DoctrineORMMappingIndex;
    use DoctrineORMMappingJoinColumn;
    use DoctrineORMMappingManyToOne;
    use DoctrineORMMappingOneToMany;
    use DoctrineORMMappingTable;
    use DoctrineORMMappingUniqueConstraint;
    
    #[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 AppEntity;
    
    use DoctrineCommonCollectionsArrayCollection;
    use DoctrineCommonCollectionsCollection;
    use DoctrineORMMappingColumn;
    use DoctrineORMMappingEntity;
    use DoctrineORMMappingGeneratedValue;
    use DoctrineORMMappingId;
    use DoctrineORMMappingOneToMany;
    
    #[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;
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search