Skip to content

feat: Add Timber::get_taxonomy() and Timber::get_taxonomies() - #3284

Open
Levdbas wants to merge 6 commits into
2.xfrom
features/get_taxonomies
Open

feat: Add Timber::get_taxonomy() and Timber::get_taxonomies()#3284
Levdbas wants to merge 6 commits into
2.xfrom
features/get_taxonomies

Conversation

@Levdbas

@Levdbas Levdbas commented Sep 2, 2026

Copy link
Copy Markdown
Member

Issue

Timber has get_term() and get_terms(), but no way to get the taxonomy itself. That means everything you configure in register_taxonomy() – most importantly the labels – is out of reach from a template. To display something as simple as “All Genres”, you currently have to drop down to {{ fn('get_taxonomy', 'genre').labels.all_items }}, which is exactly the kind of thing the object API exists to avoid.

The workaround that gets suggested is to assemble the data by hand in PHP:

$taxonomy = get_taxonomy('gamme');

$context['filters'][] = [
    'name' => $taxonomy->name,
    'labels' => $taxonomy->labels,
    'terms' => Timber::get_terms(['taxonomy' => 'gamme']),
];

Related:

Solution

Adds a Timber\Taxonomy object and the two API methods to get it.

Timber\Taxonomy extends Timber\Core and is built the same way Timber\Term is – a protected constructor plus static build(WP_Taxonomy) – so it can’t be instantiated directly and always goes through the factory. init() imports the WP_Taxonomy object, which puts everything from register_taxonomy() (labels, hierarchical, rewrite, show_in_rest, …) directly on the object. On top of that it adds:

  • terms($query_args = [], $options = []) – delegates to Timber::get_terms(), so you get real Timber\Term objects and the Term Class Map keeps applying. Terms are only queried when this is called, and the unfiltered result is memoized.
  • title() – the human-readable label. Worth having because name on a taxonomy is the registration name (post_tag), while name on a Timber\Term is the label. title() removes that trap and matches Post::title() and Term::title().
  • default_term() – the term registered through the default_term argument of register_taxonomy(), resolved from the default_term_{$taxonomy} option and returned as a Timber\Term.
  • can_edit() and edit_link() – the term overview in the admin, guarded by the taxonomy’s own manage_terms capability. Same pair as on Post and Term.
  • post_types() – the post types the taxonomy is registered for, as Timber\PostType objects.
  • wp_object() and __toString() (returns the taxonomy name), matching Timber\Term.

Timber\Factory\TaxonomyFactory mirrors TermFactory. Its from() accepts a taxonomy name, a list of names, an arguments array, a WP_Taxonomy, or an existing Timber\Taxonomy (idempotent). It introduces the timber/taxonomy/classmap and timber/taxonomy/class filters, with the same array-or-callable shape as the other class maps.

Timber::get_taxonomy() falls back to the taxonomy of the currently queried term when called without arguments, the same way Timber::get_term() falls back to the queried term. Timber::get_taxonomies() defaults to public taxonomies and returns an array keyed by taxonomy name. Both are registered as Twig functions.

Two implementation details worth calling out for review:

  • object_type. WP_Taxonomy::$object_type (the post types the taxonomy is attached to) collides with Timber\Core::$object_type. Timber\Taxonomy extends Core rather than CoreEntity – taxonomies have no meta – so Core::$object_type is only used for meta lookups and deprecation notices, neither of which apply here. WordPress’ meaning wins, which is what a template author would expect. There’s a test pinning this.
  • The post_type argument. WordPress’ own object_type argument to get_taxonomies() compares with wp_filter_object_list() and therefore requires an exact match of the registered post type array, so ['object_type' => ['recipe']] will not find a taxonomy registered for ['post', 'recipe']. The post_type alias is resolved through get_object_taxonomies() and then intersected with the result of the remaining arguments, so it behaves the way you’d expect and composes with the other arguments.- default_term. Because a default_term() method exists and no matching property is declared, Core::import() skips the raw default_term registration array from WP_Taxonomy and {{ taxonomy.default_term }} resolves to the method instead. That’s deliberate – the resolved Timber\Term is more useful than the registration arguments – and follows the same approach as the protected Term::$description.

Impact

Additive only. No existing class, method, filter or behaviour changes, so there is no backwards compatibility concern.

Performance impact is limited by design: terms are lazy, so Timber::get_taxonomies() on a site with many taxonomies does not fire a term query per taxonomy. Building a Taxonomy is just get_taxonomy() plus a property import, with no database access.

Usage Changes

// Get a taxonomy by name.
$taxonomy = Timber::get_taxonomy('genre');

// Use the taxonomy of the currently queried term archive.
$taxonomy = Timber::get_taxonomy();

// All public taxonomies, keyed by name.
$taxonomies = Timber::get_taxonomies();

// A list of taxonomy names.
$taxonomies = Timber::get_taxonomies(['category', 'genre']);

// Any get_taxonomies() argument, plus a post_type alias.
$taxonomies = Timber::get_taxonomies([
    'post_type' => 'recipe',
    'hierarchical' => true,
]);
{% set genre = get_taxonomy('genre') %}

<h1>{{ genre.title }}</h1>

{% for term in genre.terms %}
    <a href="{{ term.link }}">{{ term.title }}</a>
{% endfor %}

{% if genre.can_edit %}
    <a href="{{ genre.edit_link }}">Manage genres</a>
{% endif %}

New filters: timber/taxonomy/classmap and timber/taxonomy/class.

New Twig functions: get_taxonomy() and get_taxonomies().

Docs: a new Taxonomies guide, a The Taxonomy Class Map section in the Class Maps guide, and a cross-link from the Terms guide.

Considerations

  • Timber::get_taxonomy_by() is deliberately not included. Unlike terms and users, taxonomies are only ever addressed by name, so there is no second field to look up by.
  • Timber\Term::$taxonomy is still a string rather than a Timber\Taxonomy object. Changing it would be a breaking change and would risk queries in unexpected places, so it stayed as is. Timber::get_taxonomy(term.taxonomy) covers the gap.
  • While writing the laziness test I noticed each Timber term query fires pre_get_terms twice: WP_Term_Query::__construct() already runs the query, and TermFactory::from_wp_term_query() then calls get_terms() on the same instance again. WordPress’ object cache absorbs the second fetch so it isn’t a duplicate database hit, and it predates this PR, but it may be worth a separate look.

Testing

Yes – 34 tests, all included.

tests/TaxonomyTest.php covers getting a taxonomy by name and from a WP_Taxonomy, null for an unregistered name, __toString(), the object_type/post_types() behaviour described above, terms() with and without query arguments, and all four get_taxonomies() input shapes including post_type on its own and combined with another argument.

The added API methods are covered too: title() in PHP and Twig, default_term() against a taxonomy registered with a default_term as well as one without, and can_edit()/edit_link() for both an administrator and a subscriber.

Laziness is asserted rather than assumed. testTermsAreLazy counts pre_get_terms and checks that getting a taxonomy runs no term query at all, that the first terms() call does, that a second unfiltered call is served from the memoized result, and that passing arguments bypasses it.

Class map behaviour is covered from both directions. tests/Factory/TaxonomyFactoryTest.php tests the array form, the callable form and the timber/taxonomy/class filter, plus invalid input. tests/TaxonomyTest.php then does it end to end with a Genre extends Taxonomy fixture that adds top_level_terms() and term_count(), asserting that the class map applies through both get_taxonomy() and get_taxonomies(), that unmapped taxonomies still get the base class, that the methods return the right values against a real parent/child term set, and that they render from Twig:

{% set genre = get_taxonomy('genre') %}
{{ genre.title }} ({{ genre.term_count }}): {{ genre.top_level_terms|join(', ') }}
{# => Genres (3): Ambient, Jazz #}

The full suite (1314 tests), PHPStan and ECS all pass.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

API Surface Changes

If any of the additions below are not intended as public API, mark them with @internal in the docblock.

New API Surface

Classes

Methods

Properties

@Levdbas Levdbas linked an issue Sep 2, 2026 that may be closed by this pull request
@codecov

codecov Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.58120% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.42%. Comparing base (bc1298f) to head (01d6e0e).

Files with missing lines Patch % Lines
src/Factory/TaxonomyFactory.php 96.82% 2 Missing ⚠️
src/Timber.php 86.66% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##                2.x    #3284      +/-   ##
============================================
+ Coverage     90.28%   90.42%   +0.14%     
- Complexity     1684     1728      +44     
============================================
  Files            59       61       +2     
  Lines          5139     5256     +117     
============================================
+ Hits           4640     4753     +113     
- Misses          499      503       +4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coveralls

coveralls commented Sep 2, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 90.21% (+0.1%) from 90.065% — features/get_taxonomies into 2.x

@nlemoine nlemoine left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Levdbas Thanks for working on this. I think a few changes are still needed. Happy to help if you need.

Comment thread src/Factory/TaxonomyFactory.php Outdated
Comment thread src/Taxonomy.php Outdated
Comment thread src/Taxonomy.php
Comment thread tests/TaxonomyTest.php Outdated
Comment thread tests/TaxonomyTest.php Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Timber::get_taxonomy and Timber::get_taxonomies

3 participants