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.
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
config/bundles.php (there is almost certainly no Flex recipe — expect manual registration):
SymfonyContrib\TaxonomyBundle\TaxonomyBundle::class => ['all' => true],
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.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.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.
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.
"repositories" with a VCS entry) rather than depending on Packagist tags you don't control, and pin to a commit.doctrine:migrations:diff own the whole schema, or copy the bundle entities into your app. Don't mix.term_id values after any bulk delete — the bundle almost certainly won't warn you.fetch="EAGER" on the association or add a repository method that hydrates terms in one batch.TermManager service — not from controllers. If the bundle writes terms directly via Doctrine, hook postPersist/postRemove listeners so your cache doesn't go stale.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.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.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.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.How can I help you explore Laravel packages today?