fpn/doctrine-extensions-taggable
Doctrine 2 Taggable extension that lets you add and manage tags on your entities. Implement the Taggable interface on your models, register the package XML metadata, and hook up the TagListener/TagManager to persist tags automatically.
Install the Package
composer require fpn/doctrine-extensions-taggable
Note: Requires Doctrine ORM. Install via:
composer require doctrine/orm
Set Up Doctrine in Laravel
laravel-doctrine/orm (experimental) or manually configure Doctrine in config/doctrine.php:
$config = new \Doctrine\ORM\Configuration();
$driverImpl = new \Doctrine\ORM\Mapping\Driver\DriverChain();
$driverImpl->addDriver(new \Doctrine\ORM\Mapping\Driver\XmlDriver(__DIR__.'/vendor/fpn/doctrine-extensions-taggable/metadata'), 'DoctrineExtensions\Taggable\Entity');
$config->setMetadataDriverImpl($driverImpl);
Register the TagListener
In a Laravel service provider (e.g., AppServiceProvider):
public function boot()
{
$em = DoctrineHelper::getEntityManager();
$tagManager = new \DoctrineExtensions\Taggable\TagManager($em);
$em->getEventManager()->addEventSubscriber(new \DoctrineExtensions\Taggable\TagListener($tagManager));
}
Create a Taggable Eloquent Model
Extend a model to implement Taggable via a trait (since Laravel doesn’t support interfaces directly):
use DoctrineExtensions\Taggable\Taggable;
use Doctrine\Common\Collections\ArrayCollection;
class Article implements Taggable
{
// Eloquent fields...
protected $tags;
public function getTaggableType(): string
{
return 'article';
}
public function getTaggableId()
{
return $this->id;
}
public function getTags()
{
$this->tags = $this->tags ?: new ArrayCollection();
return $this->tags;
}
}
First Use Case: Tag an Article
$article = Article::find(1);
$tagManager = app(\DoctrineExtensions\Taggable\TagManager::class);
$tag = $tagManager->loadOrCreateTag('laravel');
$tagManager->addTag($tag, $article);
$tagManager->saveTagging($article);
class TagService
{
protected $tagManager;
public function __construct(\DoctrineExtensions\Taggable\TagManager $tagManager)
{
$this->tagManager = $tagManager;
}
public function tagResource($resource, array $tagNames)
{
$tags = $this->tagManager->loadOrCreateTags($tagNames);
$this->tagManager->replaceTags($tags, $resource);
$this->tagManager->saveTagging($resource);
}
}
$service = new TagService($tagManager);
$service->tagResource($article, ['laravel', 'php']);
TagRepository for efficient queries:
$tagRepo = $em->getRepository('DoctrineExtensions\Taggable\Entity\Tag');
$articleIds = $tagRepo->getResourceIdsForTag('article', 'laravel');
$articles = Article::whereIn('id', $articleIds)->get();
$query = $em->createQuery(
'SELECT t FROM DoctrineExtensions\Taggable\Entity\Tag t
JOIN t.resources r WHERE r.type = :type AND t.name = :name'
)->setParameters(['type' => 'article', 'name' => 'laravel']);
$articles = Article::where('published', true)->get();
$tagManager->loadOrCreateTags(['news', 'update']);
foreach ($articles as $article) {
$tagManager->addTag($newsTag, $article);
}
$tagManager->saveTagging($articles); // Bulk save
class ArticleObserver
{
public function saved(Article $article)
{
if ($article->isDirty('tags')) {
$tagManager = app(\DoctrineExtensions\Taggable\TagManager::class);
$tagManager->replaceTags(
$tagManager->loadOrCreateTags($article->tags),
$article
);
$tagManager->saveTagging($article);
}
}
}
Avoid ORM Conflicts:
App\Doctrine\Entity).Leverage Laravel’s Service Container:
TagManager and TagListener as singletons:
$this->app->singleton(\DoctrineExtensions\Taggable\TagManager::class, function ($app) {
return new \DoctrineExtensions\Taggable\TagManager($app['doctrine.orm.entity_manager']);
});
Tag Validation:
TagManager or a custom service:
public function loadOrCreateTag(string $name): Tag
{
if (!preg_match('/^[a-z0-9_-]+$/i', $name)) {
throw new \InvalidArgumentException('Invalid tag format.');
}
return parent::loadOrCreateTag($name);
}
Caching Tag Queries:
getTagsWithCountArray:
$cacheKey = 'tags:article:count';
$tags = Cache::remember($cacheKey, now()->addHours(1), function () use ($tagRepo) {
return $tagRepo->getTagsWithCountArray('article');
});
Testing:
TagManager:
$this->mock(TagManager::class)->shouldReceive('addTag')->once();
Doctrine vs. Eloquent Schema Conflicts:
Tag table may conflict with Eloquent migrations.Event Listener Overhead:
TagListener adds event subscribers globally, which may impact performance.Taggable Interface Incompatibility:
trait TaggableTrait
{
public function getTaggableType(): string { return 'article'; }
public function getTaggableId() { return $this->id; }
public function getTags() { return $this->tags ?: new ArrayCollection(); }
}
Circular Dependencies:
Tag ↔ Article).fetch="EXTRA_LAZY" in Doctrine mappings.Transaction Management:
saveTagging() may fail silently if not wrapped in a transaction.DB::transaction(function () use ($tagManager, $article) {
$tagManager->saveTagging($article);
});
Tag Not Persisting:
saveTagging() is called after the entity is persisted ($article->save()).$config->setSQLLogger(new \Doctrine\DBAL\Logging\EchoSQLLogger());
Duplicate Tags:
loadOrCreateTag:
$name = strtolower(trim($name));
Performance Issues:
$profiler = $em->getConnection()->getConfiguration()->getSQLLogger();
$profiler->start Profiling();
How can I help you explore Laravel packages today?