Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Tag Bundle Laravel Package

fpn/tag-bundle

FPN Tag Bundle brings flexible tagging to Symfony applications. Add tags to Doctrine entities, retrieve objects by tag, and render tag clouds through a simple, configurable API. It provides ready-to-use services and integration for building searchable, categorized content.

View on GitHub
Deep Wiki
Context7

Getting Started

Treat fpn/tag-bundle as a Doctrine tagging library to lift into any project running Doctrine ORM — not as a Laravel package. There is no service provider, no Eloquent support, and the composer constraints (symfony/symfony ~2.x, PHP 5.3+) will not resolve cleanly against a modern Laravel app. The realistic paths are: (a) run Doctrine inside Laravel via laravel-doctrine/orm and wire the pieces yourself, or (b) read the roughly ten classes and port the pattern to Eloquent. Option (b) is cheaper and is what most teams should do.

Minimal steps (Doctrine-in-Laravel route):

  1. Vendor the useful classes. Try composer require fpn/tag-bundle only if constraints permit; otherwise copy Model/TaggableInterface.php, Entity/Taggable.php, Entity/Tag.php, Entity/Tagging.php, TagManager.php, TagRepository.php, and Doctrine/TaggableListener.php into your own namespace.

  2. Register the listener on the EntityManager's event manager (postLoad, prePersist, preUpdate). This listener is what makes tagging feel automatic. In a service provider:

    $em = app('em');
    $em->getEventManager()->addEventSubscriber(
        new TaggableListener(app(TagManager::class))
    );
    
  3. Bind the two class hooks (the bundle's fpn_tag.tag_class / tagging_class) to your own concrete classes:

    fpn_tag:
        tag_class: App\Doctrine\Entity\Tag
        tagging_class: App\Doctrine\Entity\Tagging
    
  4. Implement TaggableInterface on your entity and use Taggable; — this gives you addTag(), getTags(), setTags(), getTagNames(), setTagNames().

  5. First use case: tag a Post, flush, then read it back:

    $post->addTag('laravel');
    $post->addTag('doctrine');
    $em->persist($post);
    $em->flush();
    
    $tagManager->loadTagging($post); // only if postLoad isn't wired
    foreach ($post->getTags() as $tag) {
        echo $tag->getName();
    }
    

Where to look first, in order: README → Model/TaggableInterface (the contract) → Entity/Taggable (the trait, especially how tagNames differs from tags) → Doctrine/TaggableListener (the actual magic) → TagManager (loadTagging / saveTagging) → TagRepository (the queries you'll reuse or rewrite).

Implementation Patterns

Write via names, read via entities. The core idiom: a form submits a comma-separated string, you explode it into tagNames, and the listener reconciles those names into real Tag rows on flush:

$post->setTagNames(array_filter(array_map('trim', explode(',', $input))));
$em->flush();

On the read side you work with Tag entities through getTags(). Keep this asymmetry in mind — tagNames is transient/dirty state; tags is the reconciled collection.

Controller/service shape. Keep the tag plumbing out of controllers:

class PostTagger
{
    public function __construct(private TagManager $tagManager) {}

    public function sync(Post $post, string $raw): void
    {
        $names = array_unique(array_filter(array_map(
            fn ($t) => mb_strtolower(trim($t)),
            explode(',', $raw)
        )));
        $post->setTagNames($names);
    }
}

Querying by tag. The TagRepository pattern is a small join through tagging back to your resource. Port it as a query scope so the app doesn't reach into repository internals:

$posts = $em->createQueryBuilder()
    ->select('p')
    ->from(Post::class, 'p')
    ->join('p.taggings', 'tg')
    ->join('tg.tag', 't')
    ->where('t.name IN (:names)')
    ->setParameter('names', $names)
    ->getQuery()
    ->getResult();

If you port to Eloquent, this becomes morphToMany plus a scopeWithAnyTag — same shape, less ceremony.

Integration tips.

  • Eager-load the relation in list views. The listener's postLoad hydration per entity is the classic N+1 trap; a page of 50 posts can become 50+ queries.
  • Reuse the listener as a behavioral spec. Read saveTagging() before writing your port — it encodes the dedupe/attach/detach reconciliation you'd otherwise re-derive from bugs.
  • Normalize on write. Lowercase + trim + collapse internal whitespace before setTagNames(). The bundle does not do this for you.
  • Wrap tag sync in a transaction when it accompanies entity persistence, so a partial reconcile doesn't leave orphan taggings.
  • Expose getTagNames() to your serializer for API responses — it's cheaper than serializing the Tag collection and avoids leaking join-entity internals.

Gotchas and Tips

Composer will fight you. The symfony/* and php constraints are Symfony 2-era. Modern Laravel pins Symfony 6/7 components. --ignore-platform-reqs gets you an install you can't trust — treat resolution failure as a stop sign, not a workaround prompt.

tagNames vs tags is the #1 confusion. Setting tags directly bypasses the listener's reconciliation and can leave duplicate Tag rows or stale tagging joins. Prefer setTagNames() on writes and getTags() on reads.

loadTagging() is only needed if postLoad isn't firing. If you see empty tag collections after fetching, check listener registration before assuming a mapping bug. This is the single most common integration failure and it fails silently.

Polymorphic joins carry no FK to the tagged entity. Delete a Post and its tagging rows stay behind. You own cleanup: either a Doctrine lifecycle hook on your entity or a scheduled DELETE FROM tagging WHERE taggable_type = ? AND taggable_id NOT IN (...). Orphaned rows surface weeks later as inflated tag counts and phantom search results.

Case/whitespace variance creates duplicate tags. "PHP", "php ", "Php" become three Tag rows and skew every tag cloud. Enforce a unique index on normalized name and normalize in application code — the schema alone won't save you.

Index tag_name and the composite resource key. Tag clouds and "posts tagged X" lookups are the hot paths and both are join-heavy. Without the composite index (tag_name, taggable_type, taggable_id) you'll see full scans past a few thousand rows. Add a denormalized counts table or cache once you pass ~10⁵ taggings — live GROUP BY for tag counts does not scale.

No PHP 8.x or Doctrine ORM 3 guarantees. Annotation-based mappings from this era break under ORM 3's removed API surface. If you're vendoring, convert mappings to attributes during the port rather than after.

Extension points worth keeping in a port: the TaggableInterface contract (lets non-trait entities opt in), the TagManager as an injectable seam (testable without an EntityManager), and the TagRepository as the single place query shape lives. Drop the Symfony form types and Twig helpers — those are the least portable parts.

Debugging tactic. When tags don't persist, dump getTagNames() immediately before and after flush(). If the array is populated but no rows appear, the listener isn't attached. If rows appear but duplicates do, normalization is missing. Those two checks cover nearly every failure mode.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package