feat: Add Timber::get_taxonomy() and Timber::get_taxonomies() - #3284
Open
Levdbas wants to merge 6 commits into
Open
feat: Add Timber::get_taxonomy() and Timber::get_taxonomies()#3284Levdbas wants to merge 6 commits into
Levdbas wants to merge 6 commits into
Conversation
Contributor
API Surface ChangesIf any of the additions below are not intended as public API, mark them with New API SurfaceClasses
Methods
Properties
|
Codecov Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
nlemoine
requested changes
Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue
Timber has
get_term()andget_terms(), but no way to get the taxonomy itself. That means everything you configure inregister_taxonomy()– most importantly thelabels– 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:
Related:
Timber::get_taxonomyandTimber::get_taxonomies#3282Solution
Adds a
Timber\Taxonomyobject and the two API methods to get it.Timber\TaxonomyextendsTimber\Coreand is built the same wayTimber\Termis – a protected constructor plusstatic build(WP_Taxonomy)– so it can’t be instantiated directly and always goes through the factory.init()imports theWP_Taxonomyobject, which puts everything fromregister_taxonomy()(labels,hierarchical,rewrite,show_in_rest, …) directly on the object. On top of that it adds:terms($query_args = [], $options = [])– delegates toTimber::get_terms(), so you get realTimber\Termobjects 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 becausenameon a taxonomy is the registration name (post_tag), whilenameon aTimber\Termis the label.title()removes that trap and matchesPost::title()andTerm::title().default_term()– the term registered through thedefault_termargument ofregister_taxonomy(), resolved from thedefault_term_{$taxonomy}option and returned as aTimber\Term.can_edit()andedit_link()– the term overview in the admin, guarded by the taxonomy’s ownmanage_termscapability. Same pair as onPostandTerm.post_types()– the post types the taxonomy is registered for, asTimber\PostTypeobjects.wp_object()and__toString()(returns the taxonomy name), matchingTimber\Term.Timber\Factory\TaxonomyFactorymirrorsTermFactory. Itsfrom()accepts a taxonomy name, a list of names, an arguments array, aWP_Taxonomy, or an existingTimber\Taxonomy(idempotent). It introduces thetimber/taxonomy/classmapandtimber/taxonomy/classfilters, 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 wayTimber::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 withTimber\Core::$object_type.Timber\TaxonomyextendsCorerather thanCoreEntity– taxonomies have no meta – soCore::$object_typeis 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.post_typeargument. WordPress’ ownobject_typeargument toget_taxonomies()compares withwp_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']. Thepost_typealias is resolved throughget_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 adefault_term()method exists and no matching property is declared,Core::import()skips the rawdefault_termregistration array fromWP_Taxonomyand{{ taxonomy.default_term }}resolves to the method instead. That’s deliberate – the resolvedTimber\Termis more useful than the registration arguments – and follows the same approach as the protectedTerm::$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 aTaxonomyis justget_taxonomy()plus a property import, with no database access.Usage Changes
{% 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/classmapandtimber/taxonomy/class.New Twig functions:
get_taxonomy()andget_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::$taxonomyis still a string rather than aTimber\Taxonomyobject. 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.pre_get_termstwice:WP_Term_Query::__construct()already runs the query, andTermFactory::from_wp_term_query()then callsget_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.phpcovers getting a taxonomy by name and from aWP_Taxonomy,nullfor an unregistered name,__toString(), theobject_type/post_types()behaviour described above,terms()with and without query arguments, and all fourget_taxonomies()input shapes includingpost_typeon 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 adefault_termas well as one without, andcan_edit()/edit_link()for both an administrator and a subscriber.Laziness is asserted rather than assumed.
testTermsAreLazycountspre_get_termsand checks that getting a taxonomy runs no term query at all, that the firstterms()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.phptests the array form, the callable form and thetimber/taxonomy/classfilter, plus invalid input.tests/TaxonomyTest.phpthen does it end to end with aGenre extends Taxonomyfixture that addstop_level_terms()andterm_count(), asserting that the class map applies through bothget_taxonomy()andget_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.