@alexg-nn

Как правильно реализовать теги в Symfony?

Добрый день!

Пытаюсь решить в общем то стандартную задачу - создать статью с тегами. ТЗ следующее:
  1. У статьи может быть 0 или более тегов
  2. Теги разделяемы между 1 и более статей
  3. При удалении статьи теги должны удаляться, если больше нет статей с такими тегами
  4. Удаление тега из статьи должно удалять его из системы, если больше нет статей с таким тегом
  5. Удаление тега не должно приводить к удалению статьи, но у всех статей он должен пропадать


Задачу начал решать широко описанным способом - через Many-to-many ассоциацию.
Основной код такой:
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);
        }
    }
}


Используется это примерно так
$article = new Article();
$tagifier = new Tagifier(...);
$tagifier->tagify($article, 'first tag', 'second tag');
$articleRepo->save($article);


Маппинг типичный вроде бы:
<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>


В текущем состоянии выполняются все задачи, кроме пунктов 3 и 4 - теги после удаления статьи (или удаления тега из статьи) продолжают оставаться в своей таблице tags, хотя из таблицы связей записи исчезают.

Пробовал поиграть с orphanRemoval=true и cascade="remove", но при этом всё также нарушаются требования 3 и 4 только уже по другому - теги начинают просто удаляться без учёта использования в других статьях.

Я так понимаю, чисто маппингом оставшуюся задачу с неиспользуемыми тегами не решить, и надо работать с событиями доктрины, но мне в голову не приходит, на что тут подписаться и как это должно выглядеть.

Буду признателен за подсказки в этом направлении.
  • Вопрос задан
  • 837 просмотров
Решения вопроса 1
@Flying
п.п.3 и 4 реализуются довольно просто через lifecycle events в Doctrine. Просто на preRemove вешаете обработчик, в нём проверяете тип entity и, если это что-то подходящее (статья или тег) то проверяете связи удаляемой entity и по необходимости подчищаете.
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы