A
A
alexg-nn2019-08-22 23:07:38
symfony
alexg-nn, 2019-08-22 23:07:38

How to properly implement tags in Symfony?

Good afternoon!
I'm trying to solve, in general, a standard task - to create an article with tags. TK is the following:

  1. An article can have 0 or more tags
  2. Tags are shared between 1 or more articles
  3. When deleting an article, tags should be deleted if there are no more articles with such tags
  4. Removing a tag from an article should remove it from the system if there are no more articles with that tag
  5. Deleting a tag should not delete the article, but it should disappear for all articles

I began to solve the problem in a widely described way - through the Many-to-many association.
The main code is this:
class Article
{
    protected $tags;

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

    public function tags(): Collection
    {
        return $this->tags;
    }

    public function addTag(Tag $tag): self
    {
        if($this->hasTag($tag) == false) {
            $this->tags->add($tag);
        }
        return $this;
    }

    public function removeTag(Tag $tag): self
    {
        if($this->hasTag($tag)) {
            $this->tags->removeElement($tag);
        }
        return $this;
    }

    public function hasTag(Tag $tag): bool
    {
        return $this->tags->contains($tag);
    }
}

class Tag
{
    protected $tagged;

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

    public function tagged(): Collection
    {
        return $this->tagged;
    }

    public function addTagged(Article $tagged): self
    {
        if($this->hasTagged($tagged) == false) {
            $this->tagged->add($tagged);
            $tagged->addTag($this);
        }
        return $this;
    }

    public function removeTagged(Article $tagged): self
    {
        if($this->hasTagged($tagged)) {
            $this->tagged->removeElement($tagged);
            $tagged->removeTag($this);
        }
        return $this;
    }

    public function hasTagged(Article $tagged): bool
    {
        return $this->tagged->contains($tagged);
    }
}

final class Tagifier
{
    private $tagRepository;

    public function __construct(TagRepository $tagRepository)
    {
        $this->tagRepository = $tagRepository;
    }

    public function tagify(Article $tagged, string ...$tags)
    {
        foreach ($tagged->tags() as $tag) {
            $tag->removeTagged($tagged);
        }

        $tagged->tags()->clear();

        foreach($tags as $tagName) {
            $tag = $this->tagRepository->getByName($tagName);
            if(is_null($tag)) {
                $tag = new Tag(/* конструимрование из $tagName */);
            }

            $tagged->addTag($tag);
        }
    }
}

It's used like this
$article = new Article();
$tagifier = new Tagifier(...);
$tagifier->tagify($article, 'first tag', 'second tag');
$articleRepo->save($article);

Mapping is typical like:
<entity name="Article" table="articles">
    <!-- ... -->
    <many-to-many field="tags" target-entity="Tag" inversed-by="tagged">
        <cascade>
            <cascade-persist/>
        </cascade>
        <join-table name="article_tags">
            <join-columns>
                <join-column name="article_id" on-delete="CASCADE" referenced-column-name="id"/>
            </join-columns>
            <inverse-join-columns>
                <join-column name="tag_id" on-delete="CASCADE" referenced-column-name="id"/>
            </inverse-join-columns>
        </join-table>
    </many-to-many>
</entity>

<entity name="Tag" table="tags">
    <!-- ... -->
    <many-to-many field="tagged" mapped-by="tags" target-entity="Article"/>
</entity>

In the current state, all tasks are performed, except for points 3 and 4 - tags after deleting an article (or deleting a tag from an article) continue to remain in their tags table, although records disappear from the link table.
I tried to play with orphanRemoval=true and cascade="remove", but at the same time, requirements 3 and 4 are also violated, only in a different way - tags are simply removed without taking into account the use in other articles.
As I understand it, the remaining task with unused tags cannot be solved purely by mapping, and I need to work with doctrine events, but it doesn’t occur to me what to subscribe to here and how it should look.
I would be grateful for tips in this direction.

Answer the question

In order to leave comments, you need to log in

1 answer(s)
F
Flying, 2019-08-22
@alexg-nn

items 3 and 4 are implemented quite simply through lifecycle events in Doctrine. Just hang a handler on preRemove , check the entity type in it and, if it is something suitable (an article or a tag), then check the links of the entity being removed and clean it up if necessary.

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question