Giter Site home page Giter Site logo

Comments (7)

jimiero avatar jimiero commented on August 16, 2024

@tetranz any suggestions here?

from select2entity-bundle.

tetranz avatar tetranz commented on August 16, 2024

It's been a long time since I've done anything with tags but I had a quick play with this today.

I think duplicate tags are something you need to deal with in your own code when you persist the tag object. If you detect that the tag is new, you need to do a query or something to check if that unique value already exists and if it does, don't try to add it.

from select2entity-bundle.

jimiero avatar jimiero commented on August 16, 2024

@tetranz thanks for your reply, seems to me the bundle itself does the add new tag, all I had done on my side was to enable the add new feature with:

'allow_add' => [ 'enabled' => true, 'new_tag_text' => ' (NEW)', 'new_tag_prefix' => '__', 'tag_separators' => '[",", " "]' ],

How can i do those verifications on my form?

from select2entity-bundle.

tetranz avatar tetranz commented on August 16, 2024

The bundle creates an instance of the new tag entity but it doesn't persist it to the database.
I had further play with this. I have an entity called Region and an entity called Tag. There is a many to many relationship between Region and Tag with Region being the controlling entity. My Tag entity has an auto integer id primary key and a string Name. I set Name to be unique so doctrine created a unique index.

In my controller I added this:

        foreach($region->getTags() as $tag) {
            if (empty($tag->getId())) {
                $entityManager->persist($tag);
            }
        }

That persists new tags. I can't reproduce your problem if a tag with that name already exists. The tags in region that are returned from $region->getTags() have the correct ids and nothing further needs to happen.

As I would expect, I can reproduce your problem if I open my app in another browser tab. I add a new tag in one tab but don't submit the form. Now add the same tag in the other tab and submit the form. Now go back to the other tab and submit. That reproduces the problem because both submits are trying to add the same tag. I don't think that is what you're referring to but I fixed that with this:

        foreach($region->getTags() as $tag) {
            if (empty($tag->getId())) {
                $existingTag = $this->getDoctrine()->getRepository(Tag::class)
                    ->findOneBy(['name' => $tag->getName()]);

                if (empty($existingTag)) {
                    $entityManager->persist($tag);
                }
                else {
                    $region->removeTag($tag);
                    $region->addTag($existingTag);
                }
            }
        }

That checks if the new tag already exists and substitutes the tag saved moments ago by the other browser tab / user. Even that's not perfect because it is not an atomic operation so you could still get an error with unfortunate timing between two users. You really need a try / catch around the $entityManager->persist($tag); and use the unique index on the db as the ultimate atomic operation.

But somehow you seem to be having a more general problem of simple selecting a tag that already exists. I'm not sure what happening for you.

The Tag field on my form has this:

            'allow_add' => [
                'enabled' => true,
                'new_tag_text' => '',
            ],

from select2entity-bundle.

jimiero avatar jimiero commented on August 16, 2024

@tetranz Appreciate your assistance, in my case I use the bundle to add tags to an entity like:

            ->add('tags', Select2EntityType::class, array(
                'required' => true,
                'label' => 'Article tags',
                'multiple' => true,
                'remote_route' => 'ajax-tags',
                'class' => 'AppBundle\Entity\Tags',
                'text_property' => 'name',
                'minimum_input_length' => 2,
                'page_limit' => null,
                'placeholder' => 'Select tags',
                'attr' => array('style' => 'width: 100%;'),
                'allow_add' => [
                    'enabled' => true,
                    'new_tag_text' => ' (NEW)',
                    'new_tag_prefix' => '__',
                    'tag_separators' => '[",", ""]'
                ],
            ))

I have no check inside the form, other than this:

` $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {

`

from select2entity-bundle.

moldwebs avatar moldwebs commented on August 16, 2024

->add('tags', Select2EntityType::class, [
'label' => 'Tags',
'placeholder' => '',
'class' => TxTag::class,
'property' => 'title',
'text_property' => 'title',
'multiple' => true,
'allow_add' => [
'enabled' => true,
'new_tag_text' => ' (+)',
'new_tag_prefix' => '__',
'tag_separators' => '[","]'
],
'transformer' => EntitiesToPropertyTransformer::class
])

namespace App\Component\DataTransformer;

class EntitiesToPropertyTransformer extends \Tetranz\Select2EntityBundle\Form\DataTransformer\EntitiesToPropertyTransformer
{

/**
 * Transform array to a collection of entities
 *
 * @param array $values
 * @return array
 */
public function reverseTransform($values)
{
    if (!is_array($values) || empty($values)) {
        return array();
    }

    // add new tag entries
    $newObjects = array();
    $tagPrefixLength = strlen($this->newTagPrefix);
    foreach ($values as $key => $value) {
        $cleanValue = strtolower(trim(substr($value, $tagPrefixLength)));
        $valuePrefix = substr($value, 0, $tagPrefixLength);
        if ($valuePrefix == $this->newTagPrefix) {

            $entity = $this->em->createQueryBuilder()
                ->select('entity')
                ->from($this->className, 'entity')
                ->where('entity.'.$this->textProperty.' = :value')
                ->setParameter('value', $cleanValue)
                ->getQuery()
                ->getOneOrNullResult();

            if ($entity) {
                $values[$key] = $entity->getId();
            } else {
                $object = new $this->className;
                $this->accessor->setValue($object, $this->textProperty, $cleanValue);
                $newObjects[] = $object;
                unset($values[$key]);
            }
        }
    }

    // get multiple entities with one query
    $entities = $this->em->createQueryBuilder()
        ->select('entity')
        ->from($this->className, 'entity')
        ->where('entity.'.$this->primaryKey.' IN (:ids)')
        ->setParameter('ids', $values)
        ->getQuery()
        ->getResult();

      // this will happen if the form submits invalid data

// if (count($entities) != count($values)) {
// throw new TransformationFailedException('One or more id values are invalid');
// }

    return array_merge($entities, $newObjects);
}

}

from select2entity-bundle.

zearg avatar zearg commented on August 16, 2024

Thanks for this solution @moldwebs, you should better format it ;)

It works to don't have duplicates tags in DB.

Think to add unique: true on your property (title on TxTagEntity in this example)

Add transformer to your form:

...
->add('tags', Select2EntityType::class, [
                'label' => 'Tags',
                'placeholder' => '',
                'class' => TxTag::class,
                'primary_key' => 'id',
                'text_property' => 'title',
                'multiple' => true,
                'allow_add' => [
                                'enabled' => true,
                                'new_tag_text' => ' ',
                                'new_tag_prefix' => '__',
                                'tag_separators' => '[","]'
                ],
                'transformer' => EntitiesToPropertyTransformer::class
])
...

Create your new transformer:

<?php

namespace App\Form\DataTransform;

use Doctrine\Persistence\ObjectManager;

class EntitiesToPropertyTransformer extends \Tetranz\Select2EntityBundle\Form\DataTransformer\EntitiesToPropertyTransformer
{
    /**
     * EntitiesToPropertyTransformer constructor.
     *
     * @param ObjectManager $em
     * @param               $class
     * @param null          $textProperty
     * @param string        $primaryKey
     * @param string        $newTagPrefix
     * @param string        $newTagText
     */
    public function __construct(ObjectManager $em, $class, $textProperty = null, $primaryKey = 'id', $newTagPrefix = '__', $newTagText = ''){
        // Reconstruct parent to prevent $newTagText to be null (because we set empty string in config file) and get a bad value in parent constructor
        parent::__construct($em, $class, $textProperty, $primaryKey, $newTagPrefix, $newTagText);
    }

    /**
     * Transform array to a collection of entities
     *
     * @param array $values
     * @return array
     */
    public function reverseTransform($values)
    {
        // Should always be a collection (array)
        if (!is_array($values) || empty($values)) {
            return [];
        }
        
        $newObjects = array();
        $newTagPrefixLength = strlen($this->newTagPrefix);
        
        foreach ($values as $key => $value) {
            $cleanValue = strtolower(trim(substr($value, $newTagPrefixLength)));
            $valuePrefix = substr($value, 0, $newTagPrefixLength);

            // If it's a tag that should be added to DB
            if ($valuePrefix === $this->newTagPrefix) {
                // Search for tag presence in DB
                $entity = $this->em->createQueryBuilder()
                    ->select('entity')
                    ->from($this->className, 'entity')
                    ->where('entity.'.$this->textProperty.' = :value')
                    ->setParameter('value', $cleanValue)
                    ->getQuery()
                    ->getOneOrNullResult();

                // If tag already exist in DB
                if ($entity) {
                    $values[$key] = $entity->getId();
                }
                // Transform new and non-existent tag into new Object
                else {
                    $object = new $this->className;
                    $this->accessor->setValue($object, $this->textProperty, $cleanValue);
                    $newObjects[] = $object;
                    unset($values[$key]);
                }
            }
        }

        // Get each tags already presents before
        $entities = $this->em->createQueryBuilder()
            ->select('entity')
            ->from($this->className, 'entity')
            ->where('entity.'.$this->primaryKey.' IN (:ids)')
            ->setParameter('ids', $values)
            ->getQuery()
            ->getResult();

        return array_merge($entities, $newObjects);
    }
}

from select2entity-bundle.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.