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

timber / timber / 20695674007

04 Jan 2026 04:14PM UTC coverage: 89.681% (+1.5%) from 88.211%
20695674007

push

travis-ci

nlemoine
test: Fix ancestors post tests

4615 of 5146 relevant lines covered (89.68%)

63.45 hits per line

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

63.43
/src/Term.php
1
<?php
2

3
namespace Timber;
4

5
use Stringable;
6
use Timber\Factory\TermFactory;
7
use WP_Term;
8

9
/**
10
 * Class Term
11
 *
12
 * Terms: WordPress has got 'em, you want 'em. Categories. Tags. Custom Taxonomies. You don't care,
13
 * you're a fiend. Well let's get this under control:
14
 *
15
 * @api
16
 * @example
17
 * ```php
18
 * // Get a term by its ID
19
 * $context['term'] = Timber::get_term(6);
20
 *
21
 * // Get a term when on a term archive page
22
 * $context['term_page'] = Timber::get_term();
23
 *
24
 * // Get a term with a slug
25
 * $context['team'] = Timber::get_term('patriots');
26
 * Timber::render('index.twig', $context);
27
 * ```
28
 * ```twig
29
 * <h2>{{ term_page.name }} Archives</h2>
30
 * <h3>Teams</h3>
31
 * <ul>
32
 *     <li>{{ st_louis.name}} - {{ st_louis.description }}</li>
33
 *     <li>{{ team.name}} - {{ team.description }}</li>
34
 * </ul>
35
 * ```
36
 * ```html
37
 * <h2>Team Archives</h2>
38
 * <h3>Teams</h3>
39
 * <ul>
40
 *     <li>St. Louis Cardinals - Winner of 11 World Series</li>
41
 *     <li>New England Patriots - Winner of 6 Super Bowls</li>
42
 * </ul>
43
 * ```
44
 */
45
class Term extends CoreEntity implements Stringable
46
{
47
    /**
48
     * The underlying WordPress Core object.
49
     *
50
     * @since 2.0.0
51
     *
52
     * @var WP_Term|null
53
     */
54
    protected ?WP_Term $wp_object = null;
55

56
    public $object_type = 'term';
57

58
    public static $representation = 'term';
59

60
    public $_children;
61

62
    /**
63
     * @api
64
     * @var string the human-friendly name of the term (ex: French Cuisine)
65
     */
66
    public $name;
67

68
    /**
69
     * @api
70
     * @var string the WordPress taxonomy slug (ex: `post_tag` or `actors`)
71
     */
72
    public $taxonomy;
73

74
    /**
75
     * @internal
76
     */
77
    protected function __construct()
102✔
78
    {
79
    }
102✔
80

81
    /**
82
     * @internal
83
     *
84
     * @param WP_Term      $wp_term The vanilla WordPress term object to build from.
85
     * @return static
86
     */
87
    public static function build(WP_Term $wp_term): static
102✔
88
    {
89
        $term = new static();
102✔
90
        $term->init($wp_term);
102✔
91
        return $term;
102✔
92
    }
93

94
    /**
95
     * The string the term will render as by default
96
     *
97
     * @api
98
     * @return string
99
     */
100
    public function __toString()
2✔
101
    {
102
        return $this->name;
2✔
103
    }
104

105
    /**
106
     *
107
     * @deprecated 2.0.0, use TermFactory::from instead.
108
     *
109
     * @param $tid
110
     * @param $taxonomy
111
     *
112
     * @return static
113
     */
114
    public static function from($tid, $taxonomy = null)
3✔
115
    {
116
        Helper::deprecated(
3✔
117
            "Term::from()",
3✔
118
            "Timber\Factory\TermFactory->from()",
3✔
119
            '2.0.0'
3✔
120
        );
3✔
121

122
        $termFactory = new TermFactory();
3✔
123
        return $termFactory->from($tid);
3✔
124
    }
125

126
    /* Setup
127
       ===================== */
128
    /**
129
     * @internal
130
     */
131
    protected function init(WP_Term $term)
102✔
132
    {
133
        $this->ID = $term->term_id;
102✔
134
        $this->id = $term->term_id;
102✔
135
        $this->wp_object = $term;
102✔
136
        $this->import($term);
102✔
137
    }
138

139
    /**
140
     * @internal
141
     * @param int|object|array $tid
142
     * @return mixed
143
     */
144
    protected function get_term($tid)
×
145
    {
146
        if (\is_object($tid) || \is_array($tid)) {
×
147
            return $tid;
×
148
        }
149
        $tid = self::get_tid($tid);
×
150

151
        if (\is_array($tid)) {
×
152
            //there's more than one matching $term_id, let's figure out which is correct
153
            if (isset($this->taxonomy) && \strlen($this->taxonomy)) {
×
154
                foreach ($tid as $term_id) {
×
155
                    $maybe_term = \get_term($term_id, $this->taxonomy);
×
156
                    if ($maybe_term) {
×
157
                        return $maybe_term;
×
158
                    }
159
                }
160
            }
161
            $tid = $tid[0];
×
162
        }
163

164
        if (isset($this->taxonomy) && \strlen($this->taxonomy)) {
×
165
            return \get_term($tid, $this->taxonomy);
×
166
        } else {
167
            global $wpdb;
168
            $query = $wpdb->prepare("SELECT taxonomy FROM $wpdb->term_taxonomy WHERE term_id = %d LIMIT 1", $tid);
×
169
            $tax = $wpdb->get_var($query);
×
170
            if (isset($tax) && \strlen((string) $tax)) {
×
171
                $this->taxonomy = $tax;
×
172
                return \get_term($tid, $tax);
×
173
            }
174
        }
175
        return null;
×
176
    }
177

178
    /**
179
     * @internal
180
     * @return int|array
181
     */
182
    protected static function get_tid(mixed $tid)
×
183
    {
184
        global $wpdb;
185
        if (\is_numeric($tid)) {
×
186
            return $tid;
×
187
        }
188
        if (\gettype($tid) === 'object') {
×
189
            $tid = $tid->term_id;
×
190
        }
191
        if (\is_numeric($tid)) {
×
192
            $query = $wpdb->prepare("SELECT term_id FROM $wpdb->terms WHERE term_id = %d", $tid);
×
193
        } else {
194
            $query = $wpdb->prepare("SELECT term_id FROM $wpdb->terms WHERE slug = %s", $tid);
×
195
        }
196
        $result = $wpdb->get_col($query);
×
197
        if ($result) {
×
198
            if (\count($result) == 1) {
×
199
                return $result[0];
×
200
            }
201
            return $result;
×
202
        }
203
        return false;
×
204
    }
205

206
    /* Public methods
207
    ===================== */
208

209
    /**
210
     * Gets the underlying WordPress Core object.
211
     *
212
     * @since 2.0.0
213
     *
214
     * @return WP_Term|null
215
     */
216
    public function wp_object(): ?WP_Term
1✔
217
    {
218
        return $this->wp_object;
1✔
219
    }
220

221
    /**
222
     * @deprecated 2.0.0, use `{{ term.edit_link }}` instead.
223
     * @return string
224
     */
225
    public function get_edit_url()
×
226
    {
227
        Helper::deprecated('{{ term.get_edit_url }}', '{{ term.edit_link }}', '2.0.0');
×
228
        return $this->edit_link();
×
229
    }
230

231
    /**
232
     * Gets a term meta value.
233
     * @deprecated 2.0.0, use `{{ term.meta('field_name') }}` instead.
234
     *
235
     * @param string $field_name The field name for which you want to get the value.
236
     * @return string The meta field value.
237
     */
238
    public function get_meta_field($field_name)
×
239
    {
240
        Helper::deprecated(
×
241
            "{{ term.get_meta_field('field_name') }}",
×
242
            "{{ term.meta('field_name') }}",
×
243
            '2.0.0'
×
244
        );
×
245
        return $this->meta($field_name);
×
246
    }
247

248
    /**
249
     * @internal
250
     * @return array
251
     */
252
    public function children()
1✔
253
    {
254
        if (!isset($this->_children)) {
1✔
255
            $children = \get_term_children($this->ID, $this->taxonomy);
1✔
256
            foreach ($children as &$child) {
1✔
257
                $child = Timber::get_term($child);
1✔
258
            }
259
            $this->_children = $children;
1✔
260
        }
261
        return $this->_children;
1✔
262
    }
263

264
    /**
265
     * Return the description of the term
266
     *
267
     * @api
268
     * @return string
269
     */
270
    public function description()
1✔
271
    {
272
        $prefix = '<p>';
1✔
273
        $desc = \term_description($this->ID, $this->taxonomy);
1✔
274
        if (\str_starts_with((string) $desc, $prefix)) {
1✔
275
            $desc = \substr((string) $desc, \strlen($prefix));
1✔
276
        }
277
        $desc = \preg_replace('/' . \preg_quote('</p>', '/') . '$/', '', (string) $desc);
1✔
278
        return \trim((string) $desc);
1✔
279
    }
280

281
    /**
282
     * Checks whether the current user can edit the term.
283
     *
284
     * @api
285
     * @example
286
     * ```twig
287
     * {% if term.can_edit %}
288
     *     <a href="{{ term.edit_link }}">Edit</a>
289
     * {% endif %}
290
     * ```
291
     * @return bool
292
     */
293
    public function can_edit(): bool
2✔
294
    {
295
        return \current_user_can('edit_term', $this->ID);
2✔
296
    }
297

298
    /**
299
     * Gets the edit link for a term if the current user has the correct rights.
300
     *
301
     * @api
302
     * @example
303
     * ```twig
304
     * {% if term.can_edit %}
305
     *    <a href="{{ term.edit_link }}">Edit</a>
306
     * {% endif %}
307
     * ```
308
     * @return string|null The edit URL of a term in the WordPress admin or null if the current user can’t edit the
309
     *                     term.
310
     */
311
    public function edit_link(): ?string
1✔
312
    {
313
        if (!$this->can_edit()) {
1✔
314
            return null;
×
315
        }
316

317
        return \get_edit_term_link($this->ID, $this->taxonomy);
1✔
318
    }
319

320
    /**
321
     * Returns a full link to the term archive page like `https://example.com/category/news`
322
     *
323
     * @api
324
     * @example
325
     * ```twig
326
     * See all posts in: <a href="{{ term.link }}">{{ term.name }}</a>
327
     * ```
328
     *
329
     * @return string
330
     */
331
    public function link()
3✔
332
    {
333
        $link = \get_term_link($this->wp_object);
3✔
334

335
        /**
336
         * Filters the link to the term archive page.
337
         *
338
         * @see   \Timber\Term::link()
339
         * @since 0.21.9
340
         *
341
         * @param string       $link The link.
342
         * @param Term $term The term object.
343
         */
344
        $link = \apply_filters('timber/term/link', $link, $this);
3✔
345

346
        /**
347
         * Filters the link to the term archive page.
348
         *
349
         * @deprecated 0.21.9, use `timber/term/link`
350
         */
351
        $link = \apply_filters_deprecated(
3✔
352
            'timber_term_link',
3✔
353
            [$link, $this],
3✔
354
            '2.0.0',
3✔
355
            'timber/term/link'
3✔
356
        );
3✔
357

358
        return $link;
3✔
359
    }
360

361
    /**
362
     * Gets a term meta value.
363
     *
364
     * @api
365
     * @deprecated 2.0.0, use `{{ term.meta('field_name') }}` instead.
366
     * @see \Timber\Term::meta()
367
     *
368
     * @param string $field_name The field name for which you want to get the value.
369
     * @return mixed The meta field value.
370
     */
371
    public function get_field($field_name = null)
1✔
372
    {
373
        Helper::deprecated(
1✔
374
            "{{ term.get_field('field_name') }}",
1✔
375
            "{{ term.meta('field_name') }}",
1✔
376
            '2.0.0'
1✔
377
        );
1✔
378

379
        return $this->meta($field_name);
1✔
380
    }
381

382
    /**
383
     * Returns a relative link (path) to the term archive page like `/category/news`
384
     *
385
     * @api
386
     * @example
387
     * ```twig
388
     * See all posts in: <a href="{{ term.path }}">{{ term.name }}</a>
389
     * ```
390
     * @return string
391
     */
392
    public function path()
2✔
393
    {
394
        $link = $this->link();
2✔
395
        $rel = URLHelper::get_rel_url($link, true);
2✔
396

397
        /**
398
         * Filters the relative link (path) to a term archive page.
399
         *
400
         * ```
401
         * add_filter( 'timber/term/path', function( $rel, $term ) {
402
         *     if ( $term->slug === 'news' ) {
403
         *        return '/category/modified-url';
404
         *     }
405
         *
406
         *     return $rel;
407
         * }, 10, 2 );
408
         * ```
409
         *
410
         * @see   \Timber\Term::path()
411
         * @since 0.21.9
412
         *
413
         * @param string       $rel  The relative link.
414
         * @param Term $term The term object.
415
         */
416
        $rel = \apply_filters('timber/term/path', $rel, $this);
2✔
417

418
        /**
419
         * Filters the relative link (path) to a term archive page.
420
         *
421
         * @deprecated 2.0.0, use `timber/term/path`
422
         */
423
        $rel = \apply_filters_deprecated(
2✔
424
            'timber_term_path',
2✔
425
            [$rel, $this],
2✔
426
            '2.0.0',
2✔
427
            'timber/term/path'
2✔
428
        );
2✔
429

430
        return $rel;
2✔
431
    }
432

433
    /**
434
     * Gets posts that have the current term assigned.
435
     *
436
     * @api
437
     * @example
438
     * Query the default posts_per_page for this Term:
439
     *
440
     * ```twig
441
     * <h4>Recent posts in {{ term.name }}</h4>
442
     *
443
     * <ul>
444
     * {% for post in term.posts() %}
445
     *     <li>
446
     *         <a href="{{ post.link }}">{{ post.title }}</a>
447
     *     </li>
448
     * {% endfor %}
449
     * </ul>
450
     * ```
451
     *
452
     * Query exactly 3 Posts from this Term:
453
     *
454
     * ```twig
455
     * <h4>Recent posts in {{ term.name }}</h4>
456
     *
457
     * <ul>
458
     * {% for post in term.posts(3) %}
459
     *     <li>
460
     *         <a href="{{ post.link }}">{{ post.title }}</a>
461
     *     </li>
462
     * {% endfor %}
463
     * </ul>
464
     * ```
465
     *
466
     * If you need more control over the query that is going to be performed, you can pass your
467
     * custom query arguments in the first parameter.
468
     *
469
     * ```twig
470
     * <h4>Our branches in {{ region.name }}</h4>
471
     *
472
     * <ul>
473
     * {% for branch in region.posts({
474
     *     post_type: 'branch',
475
     *     posts_per_page: -1,
476
     *     orderby: 'menu_order'
477
     * }) %}
478
     *     <li>
479
     *         <a href="{{ branch.link }}">{{ branch.title }}</a>
480
     *     </li>
481
     * {% endfor %}
482
     * </ul>
483
     * ```
484
     *
485
     * @param int|array $query           Optional. Either the number of posts or an array of
486
     *                                   arguments for the post query to be performed.
487
     *                                   Default is an empty array, the equivalent of:
488
     *                                   ```php
489
     *                                   [
490
     *                                     'posts_per_page' => get_option('posts_per_page'),
491
     *                                     'post_type'      => 'any',
492
     *                                     'tax_query'      => [ ...tax query for this Term... ]
493
     *                                   ]
494
     *                                   ```
495
     * @param string $post_type_or_class Deprecated. Before Timber 2.x this was a post_type to be
496
     *                                   used for querying posts OR the Timber\Post subclass to
497
     *                                   instantiate for each post returned. As of Timber 2.0.0,
498
     *                                   specify `post_type` in the `$query` array argument. To
499
     *                                   specify the class, use Class Maps.
500
     * @see https://timber.github.io/docs/v2/guides/posts/
501
     * @see https://timber.github.io/docs/v2/guides/class-maps/
502
     * @return PostQuery
503
     */
504
    public function posts($query = [], $post_type_or_class = null)
10✔
505
    {
506
        if (\is_string($query)) {
10✔
507
            Helper::doing_it_wrong(
1✔
508
                'Passing a query string to Term::posts()',
1✔
509
                'Pass a query array instead: e.g. `"posts_per_page=3"` should be replaced with `["posts_per_page" => 3]`',
1✔
510
                '2.0.0'
1✔
511
            );
1✔
512

513
            return false;
1✔
514
        }
515

516
        if (\is_int($query)) {
9✔
517
            $query = [
2✔
518
                'posts_per_page' => $query,
2✔
519
                'post_type' => 'any',
2✔
520
            ];
2✔
521
        }
522

523
        if (isset($post_type_or_class)) {
9✔
524
            Helper::deprecated(
1✔
525
                'Passing post_type_or_class',
1✔
526
                'Pass post_type as part of the $query argument. For specifying class, use Class Maps: https://timber.github.io/docs/v2/guides/class-maps/',
1✔
527
                '2.0.0'
1✔
528
            );
1✔
529

530
            // Honor the non-deprecated posts_per_page param over the deprecated second arg.
531
            $query['post_type'] ??= $post_type_or_class;
1✔
532
        }
533

534
        if (\func_num_args() > 2) {
9✔
535
            Helper::doing_it_wrong(
1✔
536
                'Passing a post class',
1✔
537
                'Use Class Maps instead: https://timber.github.io/docs/v2/guides/class-maps/',
1✔
538
                '2.0.0'
1✔
539
            );
1✔
540
        }
541

542
        $tax_query = [
9✔
543
            // Force a tax_query constraint on this term.
544
            'relation' => 'AND',
9✔
545
            [
9✔
546
                'field' => 'id',
9✔
547
                'terms' => $this->ID,
9✔
548
                'taxonomy' => $this->taxonomy,
9✔
549
            ],
9✔
550
        ];
9✔
551

552
        // Merge a clause for this Term into any user-specified tax_query clauses.
553
        $query['tax_query'] = \array_merge($query['tax_query'] ?? [], $tax_query);
9✔
554

555
        return Timber::get_posts($query);
9✔
556
    }
557

558
    /**
559
     * @api
560
     * @return string
561
     */
562
    public function title()
18✔
563
    {
564
        return $this->name;
18✔
565
    }
566

567
    /** DEPRECATED DOWN HERE
568
     * ======================
569
     **/
570

571
    /**
572
     * Get Posts that have been "tagged" with the particular term
573
     *
574
     * @api
575
     * @deprecated 2.0.0 use `{{ term.posts }}` instead
576
     *
577
     * @param int $numberposts
578
     * @return array|bool|null
579
     */
580
    public function get_posts($numberposts = 10)
1✔
581
    {
582
        Helper::deprecated('{{ term.get_posts }}', '{{ term.posts }}', '2.0.0');
1✔
583
        return $this->posts($numberposts);
1✔
584
    }
585

586
    /**
587
     * @api
588
     * @deprecated 2.0.0, use `{{ term.children }}` instead.
589
     *
590
     * @return array
591
     */
592
    public function get_children()
×
593
    {
594
        Helper::deprecated('{{ term.get_children }}', '{{ term.children }}', '2.0.0');
×
595

596
        return $this->children();
×
597
    }
598

599
    /**
600
     * Updates term_meta of the current object with the given value.
601
     *
602
     * @deprecated 2.0.0 Use `update_term_meta()` instead.
603
     *
604
     * @param string $key   The key of the meta field to update.
605
     * @param mixed  $value The new value.
606
     */
607
    public function update($key, $value)
×
608
    {
609
        Helper::deprecated('Timber\Term::update()', 'update_term_meta()', '2.0.0');
×
610

611
        /**
612
         * Filters term meta value that is going to be updated.
613
         *
614
         * @deprecated 2.0.0 with no replacement
615
         */
616
        $value = \apply_filters_deprecated(
×
617
            'timber_term_set_meta',
×
618
            [$value, $key, $this->ID, $this],
×
619
            '2.0.0',
×
620
            false,
×
621
            'This filter will be removed in a future version of Timber. There is no replacement.'
×
622
        );
×
623

624
        /**
625
         * Filters term meta value that is going to be updated.
626
         *
627
         * This filter is used by the ACF Integration.
628
         *
629
         * @deprecated 2.0.0, with no replacement
630
         */
631
        $value = \apply_filters_deprecated(
×
632
            'timber/term/meta/set',
×
633
            [$value, $key, $this->ID, $this],
×
634
            '2.0.0',
×
635
            false,
×
636
            'This filter will be removed in a future version of Timber. There is no replacement.'
×
637
        );
×
638

639
        $this->$key = $value;
×
640
    }
641
}
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