Добрый день!
Пытаюсь решить в общем то стандартную задачу - создать статью с тегами. ТЗ следующее:
- У статьи может быть 0 или более тегов
- Теги разделяемы между 1 и более статей
- При удалении статьи теги должны удаляться, если больше нет статей с такими тегами
- Удаление тега из статьи должно удалять его из системы, если больше нет статей с таким тегом
- Удаление тега не должно приводить к удалению статьи, но у всех статей он должен пропадать
Задачу начал решать широко описанным способом - через 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 только уже по другому - теги начинают просто удаляться без учёта использования в других статьях.
Я так понимаю, чисто маппингом оставшуюся задачу с неиспользуемыми тегами не решить, и надо работать с событиями доктрины, но мне в голову не приходит, на что тут подписаться и как это должно выглядеть.
Буду признателен за подсказки в этом направлении.