<commit_before><?php

namespace Oro\Bundle\TagBundle\Manager;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Util\ClassUtils;

use Oro\Bundle\TagBundle\Entity\Tag;
use Oro\Bundle\TagBundle\Helper\TaggableHelper;
use Oro\Bundle\TagBundle\Entity\TagManager as TagStorage;

class TagImportManager
{
    const TAGS_FIELD = 'tags';

    /** @var TagStorage */
    protected $tagStorage;

    /** @var TaggableHelper */
    protected $taggableHelper;

    /** @var array */
    protected $pendingTags = [];

    /**
     * @param TagStorage $tagStorage
     * @param TaggableHelper $taggableHelper
     */
    public function __construct(TagStorage $tagStorage, TaggableHelper $taggableHelper)
    {
        $this->tagStorage = $tagStorage;
        $this->taggableHelper = $taggableHelper;
    }

    /**
     * @param array $data
     *
     * @return array|null
     */
    public function denormalizeTags(array $data)
    {
        if (empty($data[static::TAGS_FIELD]) || !array_key_exists('name', $data[static::TAGS_FIELD])) {
            return;
        }

        return $this->tagStorage->loadOrCreateTags(array_map(
            'trim',
            explode(
                ',',
                $data[static::TAGS_FIELD]['name']
            )
        ));
    }

    /**
     * @param Collection|null $tags
     *
     * @return array
     */
    public function normalizeTags(Collection $tags = null)
    {
        if (!$tags) {
            return ['name' => ''];
        }

        return [
            'name' => implode(
                ', ',
                array_map(
                    function (Tag $tag) {
                        return $tag->getName();
                    },
                    $tags->toArray()
                )
            )
        ];
    }

    /**
     * @param array $convertDelimiter
     *
     * @return array Where first element is name of the rule and second is the rule itself
     */
    public function createTagRule($convertDelimiter)
    {
        return [
            'Tags',
            [
                'value' => sprintf('tags%sname', $convertDelimiter),
                'order' => PHP_INT_MAX,
            ]
        ];
    }

    /**
     * @param string $entityName
     * @param string $convertDelimiter
     * @param string $conversionType
     *
     * @return array
     */
    public function createTagHeaders($entityName, $convertDelimiter, $conversionType)
    {
        return [
            [
                'value' => sprintf('tags%sname', $convertDelimiter),
                'order' => PHP_INT_MAX,
            ]
        ];
    }

    /**
     * @param object $source
     * @param object $dest
     */
    public function moveTags($source, $dest)
    {
        $this->setTags($dest, $this->getTags($source));
        $key = spl_object_hash($source);
        if (isset($this->pendingTags[$key])) {
            unset($this->pendingTags[$key]);
        }
    }

    /**
     * @param object $entity
     */
    public function persistTags($entity)
    {
        $key = spl_object_hash($entity);
        if (isset($this->pendingTags[$key])) {
            $tags = $this->pendingTags[$key] ? new ArrayCollection($this->pendingTags[$key]) : $this->pendingTags[$key];
            $this->tagStorage->setTags($entity, $tags);
            unset($this->pendingTags[$key]);
        }

        $this->tagStorage->saveTagging($entity, false);
    }

    /**
     * @param object $entity
     * @return Collection|Tag[]
     */
    public function getTags($entity)
    {
        $key = spl_object_hash($entity);
        if (isset($this->pendingTags[$key])) {
            return $this->pendingTags[$key];
        }

        return $this->tagStorage->getTags($entity);
    }

    /**
     * @param object $entity
     * @param Collection|Tag[] $tags
     */
    public function setTags($entity, $tags)
    {
        $this->pendingTags[spl_object_hash($entity)] = $tags;
    }

    /**
     * @param object|string $entity
     *
     * @return bool
     */
    public function isTaggable($entity)
    {
        return $this->taggableHelper->isTaggable($entity);
    }

    /**
     * @param object[] $entities
     */
    public function loadTags(array $entities)
    {
        if (!$entities) {
            return;
        }

        $class = ClassUtils::getClass(reset($entities));
        if (!$this->isTaggable($class)) {
            return;
        }

        /*
         * map of entities by their ids
         * [int => object, ...]
         * where int is id of the entity and object is the entity itself
         */
        $entitiesById = array_combine(
            array_map([$this->taggableHelper, 'getEntityId'], $entities),
            $entities
        );

        /*
         * map of array of tags by related entity ids
         * [int => Tag[], ...]
         * where int is id of the related entity and Tag[] is list of the entity tags
         *
         * Tags here are loaded in 1 query for all given entities.
         * In case entity have no tags, the array of tags is empty
         */
        $tagsByEntityId = array_reduce(
            $this->tagStorage->getTagsByEntityIds(
                $class,
                array_keys($entitiesById)
            ),
            function (array $tags, array $tag) {
                $tags[$tag['entityId']][] = new Tag($tag['name']);

                return $tags;
            },
            array_fill_keys(array_keys($entitiesById), [])
        );

        /*
         * Loads tags into corresponding entities
         */
        array_walk(
            $tagsByEntityId,
            function (array $tags, $entityId) use ($entitiesById) {
                $this->tagStorage->setTags($entitiesById[$entityId], new ArrayCollection($tags));
            }
        );
    }
}
<commit_msg>BAP-9775: Add ability to import Tags from CSV<commit_after><?php

namespace Oro\Bundle\TagBundle\Manager;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Util\ClassUtils;

use Oro\Bundle\TagBundle\Entity\Tag;
use Oro\Bundle\TagBundle\Helper\TaggableHelper;
use Oro\Bundle\TagBundle\Entity\TagManager as TagStorage;

class TagImportManager
{
    const TAGS_FIELD = 'tags';

    /** @var TagStorage */
    protected $tagStorage;

    /** @var TaggableHelper */
    protected $taggableHelper;

    /** @var ArrayCollection[] */
    protected $pendingTags = [];

    /**
     * @param TagStorage $tagStorage
     * @param TaggableHelper $taggableHelper
     */
    public function __construct(TagStorage $tagStorage, TaggableHelper $taggableHelper)
    {
        $this->tagStorage = $tagStorage;
        $this->taggableHelper = $taggableHelper;
    }

    /**
     * @param array $data
     *
     * @return array|null
     */
    public function denormalizeTags(array $data)
    {
        if (empty($data[static::TAGS_FIELD]) || !array_key_exists('name', $data[static::TAGS_FIELD])) {
            return;
        }

        return $this->tagStorage->loadOrCreateTags(array_map(
            'trim',
            explode(
                ',',
                $data[static::TAGS_FIELD]['name']
            )
        ));
    }

    /**
     * @param Collection|null $tags
     *
     * @return array
     */
    public function normalizeTags(Collection $tags = null)
    {
        if (!$tags) {
            return ['name' => ''];
        }

        return [
            'name' => implode(
                ', ',
                array_map(
                    function (Tag $tag) {
                        return $tag->getName();
                    },
                    $tags->toArray()
                )
            )
        ];
    }

    /**
     * @param array $convertDelimiter
     *
     * @return array Where first element is name of the rule and second is the rule itself
     */
    public function createTagRule($convertDelimiter)
    {
        return [
            'Tags',
            [
                'value' => sprintf('tags%sname', $convertDelimiter),
                'order' => PHP_INT_MAX,
            ]
        ];
    }

    /**
     * @param string $entityName
     * @param string $convertDelimiter
     * @param string $conversionType
     *
     * @return array
     */
    public function createTagHeaders($entityName, $convertDelimiter, $conversionType)
    {
        return [
            [
                'value' => sprintf('tags%sname', $convertDelimiter),
                'order' => PHP_INT_MAX,
            ]
        ];
    }

    /**
     * @param object $source
     * @param object $dest
     */
    public function moveTags($source, $dest)
    {
        $this->setTags($dest, $this->getTags($source));
        $key = spl_object_hash($source);
        if (isset($this->pendingTags[$key])) {
            unset($this->pendingTags[$key]);
        }
    }

    /**
     * @param object $entity
     */
    public function persistTags($entity)
    {
        $key = spl_object_hash($entity);
        if (isset($this->pendingTags[$key])) {
            $this->tagStorage->setTags($entity, $this->pendingTags[$key]);
            unset($this->pendingTags[$key]);
        }

        $this->tagStorage->saveTagging($entity, false);
    }

    /**
     * @param object $entity
     * @return Collection|Tag[]
     */
    public function getTags($entity)
    {
        $key = spl_object_hash($entity);
        if (isset($this->pendingTags[$key])) {
            return $this->pendingTags[$key];
        }

        return $this->tagStorage->getTags($entity);
    }

    /**
     * @param object $entity
     * @param Collection|Tag[] $tags
     */
    public function setTags($entity, $tags)
    {
        $this->pendingTags[spl_object_hash($entity)] = is_array($tags) ? new ArrayCollection($tags) : $tags;
    }

    /**
     * @param object|string $entity
     *
     * @return bool
     */
    public function isTaggable($entity)
    {
        return $this->taggableHelper->isTaggable($entity);
    }

    /**
     * @param object[] $entities
     */
    public function loadTags(array $entities)
    {
        if (!$entities) {
            return;
        }

        $class = ClassUtils::getClass(reset($entities));
        if (!$this->isTaggable($class)) {
            return;
        }

        /*
         * map of entities by their ids
         * [int => object, ...]
         * where int is id of the entity and object is the entity itself
         */
        $entitiesById = array_combine(
            array_map([$this->taggableHelper, 'getEntityId'], $entities),
            $entities
        );

        /*
         * map of array of tags by related entity ids
         * [int => Tag[], ...]
         * where int is id of the related entity and Tag[] is list of the entity tags
         *
         * Tags here are loaded in 1 query for all given entities.
         * In case entity have no tags, the array of tags is empty
         */
        $tagsByEntityId = array_reduce(
            $this->tagStorage->getTagsByEntityIds(
                $class,
                array_keys($entitiesById)
            ),
            function (array $tags, array $tag) {
                $tags[$tag['entityId']][] = new Tag($tag['name']);

                return $tags;
            },
            array_fill_keys(array_keys($entitiesById), [])
        );

        /*
         * Loads tags into corresponding entities
         */
        array_walk(
            $tagsByEntityId,
            function (array $tags, $entityId) use ($entitiesById) {
                $this->tagStorage->setTags($entitiesById[$entityId], new ArrayCollection($tags));
            }
        );
    }
}

{'fraction_non_alphanumeric': 0.13101033393116407, 'fraction_numerical': 0.0005124263387138099, 'mean_word_length': 1.74624765478424, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 81, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '131892', 'n_tokens_mistral': 3340, 'n_tokens_neox': 3298, 'n_words': 691}