• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

timber / timber / 23939774682

03 Apr 2026 08:25AM UTC coverage: 89.805% (+0.2%) from 89.606%
23939774682

Pull #3213

travis-ci

web-flow
Merge 17a1ea827 into a29dfc003
Pull Request #3213: feat: add `Timber::get_terms([..], ['merge' => false]);`

48 of 49 new or added lines in 2 files covered. (97.96%)

7 existing lines in 1 file now uncovered.

4616 of 5140 relevant lines covered (89.81%)

63.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.9
/src/Factory/TermFactory.php
1
<?php
2

3
namespace Timber\Factory;
4

5
use InvalidArgumentException;
6
use Timber\CoreInterface;
7
use Timber\Term;
8
use WP_Term;
9
use WP_Term_Query;
10

11
/**
12
 * Internal API class for instantiating Terms
13
 */
14
class TermFactory
15
{
16
    public function from($params, array $options = [])
113✔
17
    {
18
        $options = \wp_parse_args($options, [
113✔
19
            'merge' => true,
113✔
20
        ]);
113✔
21

22
        // Single term by ID.
23
        if (\is_int($params) || (\is_string($params) && \is_numeric($params))) {
113✔
24
            return $this->from_id((int) $params);
78✔
25
        }
26

27
        // Non-query object (WP_Term, CoreInterface).
28
        if (\is_object($params) && !($params instanceof WP_Term_Query)) {
44✔
29
            return $this->from_term_object($params);
12✔
30
        }
31

32
        // Flat list of individual term IDs or objects.
33
        if ($this->is_numeric_array($params) && !$this->is_array_of_strings($params)) {
40✔
34
            return \array_map([$this, 'from'], $params);
12✔
35
        }
36

37
        // All remaining cases (taxonomy name/s, WP_Term_Query, query args array) resolve
38
        // to a list of terms that may be grouped by taxonomy.
39
        [$result, $queryParams] = $this->resolve_to_term_list($params);
30✔
40

41
        return $this->maybe_group_by_taxonomy($result, $queryParams, $options);
30✔
42
    }
43

44
    /**
45
     * Resolves any list-producing input into a [terms, queryParams] pair.
46
     *
47
     * @param mixed $params The input to resolve.
48
     * @return array The [terms, queryParams] pair.
49
     */
50
    private function resolve_to_term_list($params): array
30✔
51
    {
52
        // Single taxonomy name string.
53
        if (\is_string($params)) {
30✔
54
            return [
2✔
55
                $this->from_taxonomy_names([$params]),
2✔
56
                [
2✔
57
                    'taxonomy' => [$params],
2✔
58
                ],
2✔
59
            ];
2✔
60
        }
61

62
        if ($params instanceof WP_Term_Query) {
28✔
63
            return [$this->from_wp_term_query($params), $params];
3✔
64
        }
65

66
        // Numeric array of taxonomy name strings, e.g. ['category', 'post_tag'].
67
        if ($this->is_array_of_strings($params)) {
25✔
68
            return [
2✔
69
                $this->from_taxonomy_names($params),
2✔
70
                [
2✔
71
                    'taxonomy' => $params,
2✔
72
                ],
2✔
73
            ];
2✔
74
        }
75

76
        // Associative array of WP_Term_Query args.
77
        $query = new WP_Term_Query($this->filter_query_params($params));
23✔
78
        return [$this->from_wp_term_query($query), $params];
23✔
79
    }
80

81
    protected function from_id(int $id): ?Term
78✔
82
    {
83
        $wp_term = \get_term($id);
78✔
84

85
        if (!$wp_term) {
78✔
86
            return null;
1✔
87
        }
88

89
        return $this->build($wp_term);
77✔
90
    }
91

92
    protected function from_wp_term_query(WP_Term_Query $query)
30✔
93
    {
94
        $terms = $query->get_terms();
30✔
95

96
        $fields = $query->query_vars['fields'];
30✔
97
        if ('all' === $fields || 'all_with_object_id' === $fields) {
30✔
98
            return \array_map([$this, 'build'], $terms);
30✔
99
        }
100

101
        return $terms;
1✔
102
    }
103

104
    protected function from_term_object(object $obj): CoreInterface
12✔
105
    {
106
        if ($obj instanceof CoreInterface) {
12✔
107
            // We already have a Timber Core object of some kind
108
            return $obj;
1✔
109
        }
110

111
        if ($obj instanceof WP_Term) {
12✔
112
            return $this->build($obj);
11✔
113
        }
114

115
        throw new InvalidArgumentException(\sprintf(
1✔
116
            'Expected an instance of Timber\CoreInterface or WP_Term, got %s',
1✔
117
            $obj::class
1✔
118
        ));
1✔
119
    }
120

121
    protected function from_taxonomy_names(array $names)
4✔
122
    {
123
        return $this->from_wp_term_query(new WP_Term_Query(
4✔
124
            $this->filter_query_params([
4✔
125
                'taxonomy' => $names,
4✔
126
            ])
4✔
127
        ));
4✔
128
    }
129

130
    protected function get_term_class(WP_Term $term): string
109✔
131
    {
132
        /**
133
         * Filters the class(es) used for terms of different taxonomies.
134
         *
135
         * The default Term Class Map will contain class names mapped to the build-in post_tag and category taxonomies.
136
         *
137
         * @since 2.0.0
138
         * @example
139
         * ```
140
         * add_filter( 'timber/term/classmap', function( $classmap ) {
141
         *     $custom_classmap = [
142
         *         'expertise'   => ExpertiseTerm::class,
143
         *     ];
144
         *
145
         *     return array_merge( $classmap, $custom_classmap );
146
         * } );
147
         * ```
148
         *
149
         * @param array $classmap The term class(es) to use. An associative array where the key is
150
         *                        the taxonomy name and the value the name of the class to use for this
151
         *                        taxonomy or a callback that determines the class to use.
152
         */
153
        $map = \apply_filters('timber/term/classmap', [
109✔
154
            'post_tag' => Term::class,
109✔
155
            'category' => Term::class,
109✔
156
        ]);
109✔
157

158
        $class = $map[$term->taxonomy] ?? null;
109✔
159

160
        if (\is_callable($class)) {
109✔
161
            $class = $class($term);
1✔
162
        }
163

164
        $class ??= Term::class;
109✔
165

166
        /**
167
         * Filters the term class based on your custom criteria.
168
         *
169
         * Maybe you want to set a custom class based upon a certain category?
170
         * This allows you to filter the PHP class, utilizing data from the WP_Term object.
171
         *
172
         * @since 2.0.0
173
         * @example
174
         * ```
175
         * add_filter( 'timber/term/class', function( $class, $term ) {
176
         *     if ( get_term_meta($term->term_id, 'is_special_category', true) ) {
177
         *         return MyCustomTermClass::class;
178
         *     }
179
         *
180
         *     return $class;
181
         * }, 10, 2 );
182
         * ```
183
         *
184
         * @param string $class The class to use.
185
         * @param WP_Term $term The term object.
186
         */
187
        $class = \apply_filters('timber/term/class', $class, $term);
109✔
188

189
        return $class;
109✔
190
    }
191

192
    protected function build(WP_Term $term): CoreInterface
109✔
193
    {
194
        $class = $this->get_term_class($term);
109✔
195

196
        return $class::build($term);
109✔
197
    }
198

199
    protected function correct_tax_key(array $params)
27✔
200
    {
201
        $corrections = [
27✔
202
            'taxonomies' => 'taxonomy',
27✔
203
            'taxs' => 'taxonomy',
27✔
204
            'tax' => 'taxonomy',
27✔
205
        ];
27✔
206

207
        foreach ($corrections as $mistake => $correction) {
27✔
208
            if (isset($params[$mistake])) {
27✔
209
                $params[$correction] = $params[$mistake];
×
210
            }
211
        }
212

213
        return $params;
27✔
214
    }
215

216
    protected function correct_taxonomies($tax): array
25✔
217
    {
218
        $taxonomies = \is_array($tax) ? $tax : [$tax];
25✔
219

220
        $corrections = [
25✔
221
            'categories' => 'category',
25✔
222
            'tags' => 'post_tag',
25✔
223
            'tag' => 'post_tag',
25✔
224
        ];
25✔
225

226
        return \array_map(fn ($taxonomy) => $corrections[$taxonomy] ?? $taxonomy, $taxonomies);
25✔
227
    }
228

229
    protected function filter_query_params(array $params)
27✔
230
    {
231
        $params = $this->correct_tax_key($params);
27✔
232

233
        if (isset($params['taxonomy'])) {
27✔
234
            $params['taxonomy'] = $this->correct_taxonomies($params['taxonomy']);
25✔
235
        }
236

237
        $include = $params['term_id'] ?? null;
27✔
238
        if ($include) {
27✔
239
            $params['include'] = \is_array($include) ? $include : [$include];
×
240
        }
241

242
        return $params;
27✔
243
    }
244

245
    protected function is_numeric_array($arr)
40✔
246
    {
247
        if (!\is_array($arr)) {
40✔
248
            return false;
5✔
249
        }
250
        foreach (\array_keys($arr) as $k) {
35✔
251
            if (!\is_int($k)) {
35✔
252
                return false;
25✔
253
            }
254
        }
255
        return true;
12✔
256
    }
257

258
    protected function is_array_of_strings($arr)
35✔
259
    {
260
        if (!\is_array($arr)) {
35✔
261
            return false;
×
262
        }
263
        foreach ($arr as $v) {
35✔
264
            if (!\is_string($v)) {
35✔
265
                return false;
35✔
266
            }
267
        }
268
        return true;
2✔
269
    }
270

271
    /**
272
     * Groups results by taxonomy if merge is false and multiple taxonomies are present.
273
     *
274
     * @internal
275
     * @param array $results The query results (term objects).
276
     * @param mixed $params The original query parameters.
277
     * @param array $options The options array containing the merge setting.
278
     * @return array The results, either as-is or grouped by taxonomy.
279
     */
280
    protected function maybe_group_by_taxonomy($results, $params, array $options): mixed
30✔
281
    {
282
        if ($options['merge'] || !\is_array($results)) {
30✔
283
            return $results;
24✔
284
        }
285

286
        // Group results by taxonomy
287
        $grouped = [];
7✔
288
        foreach ($results as $term) {
7✔
289
            if ($term instanceof Term) {
7✔
290
                $grouped[$term->taxonomy][] = $term;
7✔
291
            }
292
        }
293

294
        // For WP_Term_Query objects, group by taxonomy without ordering
295
        if ($params instanceof WP_Term_Query) {
7✔
296
            return $grouped;
1✔
297
        }
298

299
        // Only group if we have multiple taxonomies
300
        if (\count($grouped) <= 1) {
6✔
301
            return $results;
1✔
302
        }
303

304
        // Sort by taxonomy order if explicitly specified in params
305
        if (\is_array($params) && isset($params['taxonomy']) && \is_array($params['taxonomy'])) {
5✔
306
            $ordered = [];
5✔
307
            foreach ($params['taxonomy'] as $taxonomy) {
5✔
308
                if (isset($grouped[$taxonomy])) {
5✔
309
                    $ordered[$taxonomy] = $grouped[$taxonomy];
5✔
310
                }
311
            }
312
            return $ordered;
5✔
313
        }
314

315
        // For simple arrays (term IDs, WP_Term objects, etc.), return flat
NEW
316
        return $results;
×
317
    }
318
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc