enhavo/taxonomy-bundle
Taxonomy bundle for the Enhavo CMS, providing flexible category and tag management on top of Symfony. A subtree split of the main Enhavo repository; see the main project for documentation and support.
This package is a subtree split of the enhavo/enhavo monorepo — the README here is intentionally empty. Treat the monorepo as your source of truth: browse src/Enhavo/Bundle/TaxonomyBundle/ and the docs/ folder there before touching anything. A standalone composer require enhavo/taxonomy-bundle only makes sense if the rest of your app is already an Enhavo project.
Minimal path to a working taxonomy:
composer require enhavo/enhavo
config/bundles.php:
Enhavo\Bundle\TaxonomyBundle\EnhavoTaxonomyBundle::class => ['all' => true],
Resources/config/routing* for your version):
# config/routes/enhavo_taxonomy.yaml
enhavo_taxonomy:
resource: '@EnhavoTaxonomyBundle/Resources/config/routing.yaml'
Taxonomy and Term. Run bin/console doctrine:migrations:diff to see what it wants to add before doctrine:migrations:migrate.category or tag) via fixtures or the admin UI, then attach terms to your own entity.Where to look first, in this order:
Entity/Taxonomy.php and Entity/Term.php — the domain model (slug, name, parent, position, children).Manager/TaxonomyManager.php, Manager/TermManager.php — the write-side API.Repository/TermRepository.php — the read-side API (findByTaxonomy(...), hierarchy helpers).Form/Type/TermType.php — how the admin term picker is built; the fastest way to understand what a term can carry.Resources/config/services.yaml + Resources/config/doctrine/*.orm.xml — service IDs and column names.First use case: create a single category taxonomy, add a handful of hierarchical terms, attach them to a Page-like entity with a ManyToMany, and render the tree in Twig. That exercises the whole bundle (entity, repository, form, template) in about an hour.
Reading a taxonomy and its terms. Work through the manager or repository rather than the EntityManager, so overridden model classes keep working:
use Enhavo\Bundle\TaxonomyBundle\Repository\TaxonomyRepository;
use Enhavo\Bundle\TaxonomyBundle\Repository\TermRepository;
$taxonomy = $taxonomyRepository->findOneBy(['slug' => 'category']);
$terms = $termRepository->findByTaxonomy($taxonomy); // ordered by position in most versions
Attaching terms to your own entity. The bundle's Term is standalone; you own the relation:
#[ORM\ManyToMany(targetEntity: Term::class)]
#[ORM\JoinTable(name: 'app_page_term')]
private Collection $terms;
Filter always by taxonomy when you present choices — terms live in a flat table shared by all taxonomies, so an unfiltered EntityType will happily let editors pick a tag where a category belongs:
$builder->add('terms', EntityType::class, [
'class' => Term::class,
'multiple' => true,
'query_builder' => fn (TermRepository $r) => $r->createByTaxonomyQueryBuilder($taxonomy),
]);
Hierarchy rendering. Term::getChildren() is the natural recursive node; a Twig macro beats a service:
{% macro tree(terms) %}
<ul>
{% for term in terms %}
<li>{{ term.name }}
{% if term.children|length %}{{ _self.tree(term.children) }}{% endif %}
</li>
{% endfor %}
</ul>
{% endmacro %}
Render only roots at the top level (term.parent is null) — otherwise you get the whole tree duplicated.
Extending the admin. Enhavo's resource system lets you override the term form without forking the bundle:
enhavo_resource:
resources:
enhavo_taxonomy.term:
form:
- App\Form\Type\ExtendedTermType
Overriding the model. Swap in your own entity via the bundle config, then make Doctrine resolve the interface to your class:
enhavo_taxonomy:
resources:
term:
classes:
model: App\Entity\Term
Pair this with resolve_target_entities so relations typed against TermInterface point at App\Entity\Term. Without that listener you get "target entity not found" at runtime, not at compile time.
Workflow tip: treat taxonomies as data, not code. Seed them in fixtures with fixed slugs (category, tag, topic) and reference those slugs in templates and queries — never by ID.
enhavo/* version is the single most common source of breakage. Pin via the metapackage.TermInterface / TaxonomyInterface and the repository interfaces, not the concrete classes — that's what makes enhavo_taxonomy.resources.*.classes.model overrides survivable.term.children is a lazy collection; rendering a deep tree fires one query per node. Add ->leftJoin('t.children', 'c')->addSelect('c') (or the bundle's findByTaxonomy with join, depending on version) in whatever path renders menus.TermRepository::findAll() returns terms from every taxonomy. Always constrain by taxonomy. Same for deletion: a term can be referenced by entities that have no relation to its taxonomy.news. Don't build routes assuming a global term slug lookup.$term->getName() may return the source string while the rendered page expects a locale-specific value; look for getTranslation() / the Translation association before assuming names are plain columns.onDelete in the ORM XML before letting editors delete nodes.position explicitly or the tree renders in insertion order.Term/Taxonomy postPersist/postUpdate/postRemove.How can I help you explore Laravel packages today?