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

Taxonomy Bundle Laravel Package

symfonycontrib/taxonomy-bundle

A Symfony bundle for managing taxonomies such as categories, tags, and vocabularies. It provides hierarchical terms, tagging support, and Doctrine-backed storage to organize and classify content across your applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: despite the "taxonomy for content organization" description, this is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no config/taxonomy.php. If you're on Laravel, you have two realistic options: (a) use it as a reference implementation and port its schema/concepts into Eloquent, or (b) stand up a small Symfony service alongside your app (rarely worth it). Everything below assumes you're either on Symfony or reading it to port.

Minimal steps on Symfony:

composer require symfonycontrib/taxonomy-bundle
  1. Register the bundle in config/bundles.php (there is almost certainly no Flex recipe — expect manual registration):
    SymfonyContrib\TaxonomyBundle\TaxonomyBundle::class => ['all' => true],
    
  2. Inspect DependencyInjection/Configuration.php before writing any config. That file is the source of truth for what's configurable (vocabulary definitions, entity class overrides, storage driver). Don't guess key names.
  3. Create the schema. The bundle ships Doctrine mappings; run:
    bin/console doctrine:migrations:diff
    bin/console doctrine:migrations:migrate
    
    Review the generated migration — bundles of this vintage often use XML/YAML mappings and may not map cleanly to your DB platform.
  4. Confirm services loaded: bin/console debug:container | grep -i taxonom.

Where to look first, in order:

  • Entity/ (or Model/) — you need to know the core objects (Vocabulary/Taxonomy, Term, and the relation entity) before anything else.
  • Resources/config/doctrine/ — mapping format and association ownership.
  • DependencyInjection/ — service IDs and the config tree.
  • Form/ and Twig/ — what UI helpers exist out of the box.

First use case: tag a Post entity with a flat, free-form vocabulary ("tags") plus a curated hierarchical one ("categories"). That's the 80% case and the fastest way to validate the package actually fits.

Implementation Patterns

Attaching taxonomy to your own entity. The idiomatic pattern for this style of bundle is a term-reference join entity rather than a direct ManyToMany. If the bundle exposes something like TermReference, your Post gets a OneToMany to it:

#[ORM\OneToMany(targetEntity: TermReference::class, mappedBy: 'owner', cascade: ['persist', 'remove'])]
private Collection $termReferences;

Prefer a reference entity over a raw join table: it gives you room for weight/position (ordering), createdAt, and per-attachment metadata without a schema migration later. If the bundle only offers a join table, wrap it.

Find-or-create terms. Don't let form submissions create duplicate terms. Centralize this in a small service and make it idempotent:

public function resolve(Vocabulary $vocab, string $name): Term
{
    return $this->terms->findOneBy([
        'vocabulary' => $vocab,
        'slug' => $this->slugger->slug($name)->lower(),
    ]) ?? $this->createTerm($vocab, $name);
}

Run the lookup on slug, not name — that's what makes it survive casing and whitespace differences.

Form integration. Use an autocomplete (Tom Select / Select2 via a Symfony UX or custom EntityType with choices loaded via AJAX). A plain EntityType with a few thousand terms will kill the form render. For free-form tagging, accept a comma-separated string and resolve it in the form's PRE_SUBMIT/model transformer.

Querying content by term. Always join through the reference entity and filter explicitly:

$qb->join('p.termReferences', 'tr')
   ->join('tr.term', 't')
   ->andWhere('t.slug IN (:slugs)')
   ->setParameter('slugs', $slugs)
   ->distinct();

For "has all these terms" semantics, GROUP BY p.id HAVING COUNT(DISTINCT t.id) = :n — a WHERE IN gives you OR semantics, which is usually not what editors mean.

Seeding vocabularies. Ship a console command or Doctrine fixture that creates vocabularies idempotently (look up by machine name first). Vocabularies are configuration-shaped data; keep them out of admin UI edits if you can, or you'll get drift between environments.

Porting to Laravel (if that's your actual goal). Reproduce the schema as three tables — vocabularies, terms (with vocabulary_id, parent_id, slug, name), and a polymorphic termables (term_id, termable_type, termable_id, weight). Add a HasTaxonomy trait with terms(), syncTerms(), and a scopeWithAnyTerm/scopeWithAllTerms. That gets you 90% of this bundle's value with zero cross-framework friction.

Gotchas and Tips

  • Adoption risk is the headline. Zero dependents and no stars means no community backstop. Expect to fork. Vendor the package ("repositories" with a VCS entry) rather than depending on Packagist tags you don't control, and pin to a commit.
  • Schema ownership is ambiguous. Bundle-provided entities plus your own migrations is a classic source of "works on my machine." Decide early: either let doctrine:migrations:diff own the whole schema, or copy the bundle entities into your app. Don't mix.
  • Deleting a term is the highest-risk operation. Check whether references cascade or orphan. Run a manual query for dangling term_id values after any bulk delete — the bundle almost certainly won't warn you.
  • Slug uniqueness is per-vocabulary, not global. "php" can exist as both a tag and a category. Enforce it with a composite unique index; the mapping may not.
  • N+1 on rendering. A tag list on an index page will fire one query per row. Force a fetch="EAGER" on the association or add a repository method that hydrates terms in one batch.
  • Caching. Term lists are read-heavy and rarely change. Cache per-vocabulary, and invalidate from a single TermManager service — not from controllers. If the bundle writes terms directly via Doctrine, hook postPersist/postRemove listeners so your cache doesn't go stale.
  • Hierarchy traversal. If the bundle uses parent_id adjacency (likely), recursive rendering is recursive. Either use WITH RECURSIVE in raw SQL or cache the materialized tree; a naive recursive Twig render is an easy timeout.
  • Config quirks. Check Configuration.php for ->cannotBeEmpty(), ->defaultValue() and enum-ish string switches. Bundle config from this era often expects exact strings and silently falls back rather than erroring.
  • Debugging checklist: bin/console debug:config taxonomy (confirm your YAML actually merged), doctrine:mapping:validate, debug:container --tag=doctrine.event_subscriber, and debug:router if it ships controllers you didn't expect.
  • Extension points worth knowing: override Twig templates via templates/bundles/TaxonomyBundle/, decorate services with decorates:, and prefer a CompilerPass over editing vendor code for behavioral tweaks. Keep a PATCHES.md alongside your fork so the next upgrade isn't archaeology.
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.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels