Bee-Lab/BeelabTagBundle

View on GitHub
src/Listener/TagSubscriber.php

Summary

Maintainability
B
4 hrs
Test Coverage
A
96%
<?php

namespace Beelab\TagBundle\Listener;

use Beelab\TagBundle\Tag\TaggableInterface;
use Beelab\TagBundle\Tag\TagInterface;
use Doctrine\Common\EventSubscriber;
use Doctrine\Common\Persistence\Mapping\MappingException as LegacyMappingException;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\Persistence\Mapping\MappingException;

/**
 * Add tags to entities that implements TaggableInterface.
 */
final class TagSubscriber implements EventSubscriber
{
    /**
     * @var \Doctrine\ORM\EntityManager
     */
    private $manager;

    /**
     * @var \Doctrine\ORM\UnitOfWork
     */
    private $uow;

    /**
     * @var TagInterface
     */
    private $tag;

    /**
     * @var bool
     */
    private $purge;

    /**
     * @param bool $purge whether to delete tags when entity is deleted
     *
     * @throws MappingException
     * @throws \InvalidArgumentException
     */
    public function __construct(string $tagClassName, bool $purge = false)
    {
        if (!\class_exists($tagClassName)) {
            if (\class_exists('Doctrine\Common\Persistence\Mapping\MappingException')) {
                throw LegacyMappingException::nonExistingClass($tagClassName);
            }
            throw MappingException::nonExistingClass($tagClassName);
        }
        $this->tag = new $tagClassName();
        if (!$this->tag instanceof TagInterface) {
            throw new \InvalidArgumentException(\sprintf('Class "%s" must implement TagInterface.', $tagClassName));
        }
        $this->purge = $purge;
    }

    public function getSubscribedEvents(): array
    {
        return ['onFlush'];
    }

    /**
     * Main method: call setTags() on entities scheduled to be inserted or updated, and
     * possibly call purgeTags() on entities scheduled to be deleted.
     */
    public function onFlush(OnFlushEventArgs $args): void
    {
        $this->manager = $args->getEntityManager();
        $this->uow = $this->manager->getUnitOfWork();
        foreach ($this->uow->getScheduledEntityInsertions() as $key => $entity) {
            if ($entity instanceof TaggableInterface) {
                $this->setTags($entity, false);
            }
        }
        foreach ($this->uow->getScheduledEntityUpdates() as $key => $entity) {
            if ($entity instanceof TaggableInterface) {
                $this->setTags($entity, true);
            }
        }
        if ($this->purge) {
            foreach ($this->uow->getScheduledEntityDeletions() as $key => $entity) {
                if ($entity instanceof TaggableInterface) {
                    $this->purgeTags($entity);
                }
            }
        }
    }

    /**
     * Do the stuff.
     *
     * @param bool $update true if entity is being updated, false otherwise
     */
    private function setTags(TaggableInterface $entity, bool $update = false): void
    {
        $tagNames = $entity->getTagNames();
        if (empty($tagNames) && !$update) {
            return;
        }
        // need to clone here, to avoid getting new tags
        $oldTags = \is_object($entityTags = $entity->getTags()) ? clone $entityTags : $entityTags;
        $tagClassMetadata = $this->manager->getClassMetadata(\get_class($this->tag));
        $repository = $this->manager->getRepository(\get_class($this->tag));
        foreach ($tagNames as $tagName) {
            $tag = $repository->findOneByName($tagName);
            if (empty($tag)) {
                // if tag doesn't exist, create it
                $tag = clone $this->tag;
                $tag->setName($tagName);
                $this->manager->persist($tag);
                // see http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#onflush
                $this->uow->computeChangeSet($tagClassMetadata, $tag);
            }
            if (!$entity->hasTag($tag)) {
                // add tag only if not already added
                $entity->addTag($tag);
            }
        }
        // if updating, need to check if some tags were removed
        if ($update) {
            foreach ($oldTags as $oldTag) {
                if (!\in_array($oldTag->getName(), $tagNames)) {
                    $entity->removeTag($oldTag);
                }
            }
        }
        // see http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#onflush
        $entityClassMetadata = $this->manager->getClassMetadata(\get_class($entity));
        $this->uow->computeChangeSets($entityClassMetadata, $entity);
    }

    /**
     * Purge oprhan tags
     * Warning: DO NOT purge tags if you have more than one entity
     * with tags, since this could lead to costraint violations.
     */
    private function purgeTags(TaggableInterface $entity): void
    {
        foreach ($entity->getTags() as $oldTag) {
            $this->manager->remove($oldTag);
        }
    }
}