• 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

94.48
/src/Post.php
1
<?php
2

3
namespace Timber;
4

5
use SimpleXMLElement;
6
use Stringable;
7
use Timber\Factory\PostFactory;
8
use Timber\Factory\UserFactory;
9
use WP_Post;
10

11
/**
12
 * Class Post
13
 *
14
 * This is the object you use to access or extend WordPress posts. Think of it as Timber's (more
15
 * accessible) version of `WP_Post`. This is used throughout Timber to represent posts retrieved
16
 * from WordPress making them available to Twig templates. See the PHP and Twig examples for an
17
 * example of what it’s like to work with this object in your code.
18
 *
19
 * @api
20
 * @example
21
 *
22
 * **single.php**
23
 *
24
 * ```php
25
 * $context = Timber::context();
26
 *
27
 * Timber::render( 'single.twig', $context );
28
 * ```
29
 *
30
 * **single.twig**
31
 *
32
 * ```twig
33
 * <article>
34
 *     <h1 class="headline">{{ post.title }}</h1>
35
 *     <div class="body">
36
 *         {{ post.content }}
37
 *     </div>
38
 * </article>
39
 * ```
40
 *
41
 * ```html
42
 * <article>
43
 *     <h1 class="headline">The Empire Strikes Back</h1>
44
 *     <div class="body">
45
 *         It is a dark time for the Rebellion. Although the Death Star has been
46
 *         destroyed, Imperial troops have driven the Rebel forces from their
47
 *         hidden base and pursued them across the galaxy.
48
 *     </div>
49
 * </article>
50
 * ```
51
 */
52
class Post extends CoreEntity implements DatedInterface, Setupable, Stringable
53
{
54
    /**
55
     * The underlying WordPress Core object.
56
     *
57
     * @since 2.0.0
58
     *
59
     * @var WP_Post|null
60
     */
61
    protected ?WP_Post $wp_object = null;
62

63
    /**
64
     * @var string What does this class represent in WordPress terms?
65
     */
66
    public $object_type = 'post';
67

68
    /**
69
     * @var string What does this class represent in WordPress terms?
70
     */
71
    public static $representation = 'post';
72

73
    /**
74
     * @internal
75
     * @var string stores the processed content internally
76
     */
77
    protected $___content;
78

79
    /**
80
     * @var string|boolean The returned permalink from WP's get_permalink function
81
     */
82
    protected $_permalink;
83

84
    /**
85
     * @var array Stores the results of the next Timber\Post in a set inside an array (in order to manage by-taxonomy)
86
     */
87
    protected $_next = [];
88

89
    /**
90
     * @var array Stores the results of the previous Timber\Post in a set inside an array (in order to manage by-taxonomy)
91
     */
92
    protected $_prev = [];
93

94
    /**
95
     * @var array Stores the results of the ancestors of the post as Timber\Posts
96
     */
97
    protected $_ancestors;
98

99
    /**
100
     * @var string Stores the CSS classes for the post (ex: "post post-type-book post-123")
101
     */
102
    protected $_css_class;
103

104
    /**
105
     * @api
106
     * @var int The numeric WordPress id of a post.
107
     */
108
    public $id;
109

110
    /**
111
     * @api
112
     * @var int The numeric WordPress id of a post, capitalized to match WordPress usage.
113
     */
114
    public $ID;
115

116
    /**
117
     * @api
118
     * @var int The numeric ID of the a post's author corresponding to the wp_user database table
119
     */
120
    public $post_author;
121

122
    /**
123
     * @api
124
     * @var string The raw text of a WP post as stored in the database
125
     */
126
    public $post_content;
127

128
    /**
129
     * @api
130
     * @var string The raw date string as stored in the WP database, ex: 2014-07-05 18:01:39
131
     */
132
    public $post_date;
133

134
    /**
135
     * @api
136
     * @var string The raw text of a manual post excerpt as stored in the database
137
     */
138
    public $post_excerpt;
139

140
    /**
141
     * @api
142
     * @var int The numeric ID of a post's parent post
143
     */
144
    public $post_parent;
145

146
    /**
147
     * @api
148
     * @var string The status of a post ("draft", "publish", etc.)
149
     */
150
    public $post_status;
151

152
    /**
153
     * @api
154
     * @var string The raw text of a post's title as stored in the database
155
     */
156
    public $post_title;
157

158
    /**
159
     * @api
160
     * @var string The name of the post type, this is the machine name (so "my_custom_post_type" as
161
     *      opposed to "My Custom Post Type")
162
     */
163
    public $post_type;
164

165
    /**
166
     * @api
167
     * @var string The URL-safe slug, this corresponds to the poorly-named "post_name" in the WP
168
     *      database, ex: "hello-world"
169
     */
170
    public $slug;
171

172
    /**
173
     * @var string Stores the PostType object for the post.
174
     */
175
    protected $__type;
176

177
    /**
178
     * Create and initialize a new instance of the called Post class
179
     * (i.e. Timber\Post or a subclass).
180
     *
181
     * @internal
182
     * @return static
183
     */
184
    public static function build(WP_Post $wp_post): static
435✔
185
    {
186
        $post = new static();
435✔
187

188
        $post->id = $wp_post->ID;
435✔
189
        $post->ID = $wp_post->ID;
435✔
190
        $post->wp_object = $wp_post;
435✔
191

192
        $data = \get_object_vars($wp_post);
435✔
193
        $data = $post->get_info($data);
435✔
194

195
        /**
196
         * Filters the imported post data.
197
         *
198
         * Used internally for previews.
199
         *
200
         * @since 2.0.0
201
         * @see   Timber::init()
202
         * @param array        $data An array of post data to import.
203
         * @param Post $post The Timber post instance.
204
         */
205
        $data = \apply_filters('timber/post/import_data', $data, $post);
435✔
206

207
        $post->import($data);
435✔
208

209
        return $post;
435✔
210
    }
211

212
    /**
213
     * If you send the constructor nothing it will try to figure out the current post id based on
214
     * being inside The_Loop.
215
     *
216
     * @internal
217
     */
218
    protected function __construct()
435✔
219
    {
220
    }
435✔
221

222
    /**
223
     * This is helpful for twig to return properties and methods see:
224
     * https://github.com/fabpot/Twig/issues/2
225
     *
226
     * This is also here to ensure that {{ post.class }} remains usable.
227
     *
228
     * @api
229
     *
230
     * @return mixed
231
     */
232
    public function __get($field)
22✔
233
    {
234
        if ('class' === $field) {
22✔
235
            return $this->css_class();
1✔
236
        }
237

238
        if ('_thumbnail_id' === $field) {
21✔
239
            Helper::doing_it_wrong(
2✔
240
                "Accessing the thumbnail ID through {{ {$this->object_type}._thumbnail_id }}",
2✔
241
                "You can retrieve the thumbnail ID via the thumbnail object {{ {$this->object_type}.thumbnail.id }}. If you need the id as stored on this post's postmeta you can use {{ {$this->object_type}.meta('_thumbnail_id') }}",
2✔
242
                '2.0.0'
2✔
243
            );
2✔
244
        }
245

246
        return parent::__get($field);
21✔
247
    }
248

249
    /**
250
     * This is helpful for twig to return properties and methods see:
251
     * https://github.com/fabpot/Twig/issues/2
252
     *
253
     * This is also here to ensure that {{ post.class }} remains usable
254
     *
255
     * @api
256
     *
257
     * @return mixed
258
     */
259
    public function __call($field, $args)
10✔
260
    {
261
        if ('class' === $field) {
10✔
262
            $class = $args[0] ?? '';
3✔
263
            return $this->css_class($class);
3✔
264
        }
265

266
        return parent::__call($field, $args);
7✔
267
    }
268

269
    /**
270
     * Gets the underlying WordPress Core object.
271
     *
272
     * @since 2.0.0
273
     *
274
     * @return WP_Post|null
275
     */
276
    public function wp_object(): ?WP_Post
1✔
277
    {
278
        return $this->wp_object;
1✔
279
    }
280

281
    /**
282
     * Sets up a post.
283
     *
284
     * Sets up the `$post` global, and other global variables as well as variables in the
285
     * `$wp_query` global that makes Timber more compatible with WordPress.
286
     *
287
     * This function will be called automatically when you loop over Timber posts as well as in
288
     * `Timber::context()`.
289
     *
290
     * @api
291
     * @since 2.0.0
292
     *
293
     * @return Post The post instance.
294
     */
295
    public function setup()
33✔
296
    {
297
        global $post;
298
        global $wp_query;
299

300
        // Mimic WordPress behavior to improve compatibility with third party plugins.
301
        $wp_query->in_the_loop = true;
33✔
302

303
        if (!$this->wp_object) {
33✔
304
            return $this;
×
305
        }
306

307
        /**
308
         * Maybe set or overwrite post global.
309
         *
310
         * We have to overwrite the post global to be compatible with a couple of WordPress plugins
311
         * that work with the post global in certain conditions.
312
         */
313
        // phpcs:ignore WordPress.WP.GlobalVariablesOverride.OverrideProhibited
314
        if (!$post || isset($post->ID) && $post->ID !== $this->ID) {
33✔
315
            $post = $this->wp_object;
22✔
316
        }
317

318
        // The setup_postdata() function will call the 'the_post' action.
319
        $wp_query->setup_postdata($this->wp_object);
33✔
320

321
        return $this;
33✔
322
    }
323

324
    /**
325
     * Resets variables after post has been used.
326
     *
327
     * This function will be called automatically when you loop over Timber posts.
328
     *
329
     * @api
330
     * @since 2.0.0
331
     *
332
     * @return Post The post instance.
333
     */
334
    public function teardown()
22✔
335
    {
336
        global $wp_query;
337

338
        $wp_query->in_the_loop = false;
22✔
339

340
        return $this;
22✔
341
    }
342

343
    /**
344
     * Determine whether or not an admin/editor is looking at the post in "preview mode" via the
345
     * WordPress admin
346
     * @internal
347
     * @return bool
348
     */
349
    protected static function is_previewing()
153✔
350
    {
351
        global $wp_query;
352
        return isset($_GET['preview']) && isset($_GET['preview_nonce']) && \wp_verify_nonce($_GET['preview_nonce'], 'post_preview_' . $wp_query->queried_object_id);
153✔
353
    }
354

355
    /**
356
     * Outputs the title of the post if you do something like `<h1>{{post}}</h1>`
357
     *
358
     * @api
359
     * @return string
360
     */
361
    public function __toString()
1✔
362
    {
363
        return $this->title();
1✔
364
    }
365

366
    protected function get_post_preview_object()
147✔
367
    {
368
        global $wp_query;
369
        if (static::is_previewing()) {
147✔
370
            $revision_id = $this->get_post_preview_id($wp_query);
9✔
371
            return Timber::get_post($revision_id);
9✔
372
        }
373
    }
374

375
    protected function get_post_preview_id($query)
9✔
376
    {
377
        $can = [
9✔
378
            \get_post_type_object($query->queried_object->post_type)->cap->edit_post,
9✔
379
        ];
9✔
380

381
        if ($query->queried_object->author_id !== \get_current_user_id()) {
9✔
382
            $can[] = \get_post_type_object($query->queried_object->post_type)->cap->edit_others_posts;
9✔
383
        }
384

385
        $can_preview = [];
9✔
386

387
        foreach ($can as $type) {
9✔
388
            if (\current_user_can($type, $query->queried_object_id)) {
9✔
389
                $can_preview[] = true;
8✔
390
            }
391
        }
392

393
        if (\count($can_preview) !== \count($can)) {
9✔
394
            return;
1✔
395
        }
396

397
        $revisions = \wp_get_post_revisions($query->queried_object_id);
8✔
398

399
        if (!empty($revisions)) {
8✔
400
            $revision = \reset($revisions);
8✔
401
            return $revision->ID;
8✔
402
        }
403

404
        return false;
×
405
    }
406

407
    /**
408
     * Updates post_meta of the current object with the given value.
409
     *
410
     * @deprecated 2.0.0 Use `update_post_meta()` instead.
411
     *
412
     * @param string $field The key of the meta field to update.
413
     * @param mixed  $value The new value.
414
     */
415
    public function update($field, $value)
1✔
416
    {
417
        Helper::deprecated('Timber\Post::update()', 'update_post_meta()', '2.0.0');
1✔
418

419
        if (isset($this->ID)) {
1✔
420
            \update_post_meta($this->ID, $field, $value);
1✔
421
            $this->$field = $value;
1✔
422
        }
423
    }
424

425
    /**
426
     * Gets a excerpt of your post.
427
     *
428
     * If you have an excerpt is set on the post, the excerpt will be used. Otherwise it will try to
429
     * pull from an excerpt from `post_content`. If there’s a `<!-- more -->` tag in the post
430
     * content, it will use that to mark where to pull through.
431
     *
432
     * @api
433
     * @see PostExcerpt
434
     *
435
     * @param array $options {
436
     *     An array of configuration options for generating the excerpt. Default empty.
437
     *
438
     *     @type int      $words     Number of words in the excerpt. Default `50`.
439
     *     @type int|bool $chars     Number of characters in the excerpt. Default `false` (no
440
     *                               character limit).
441
     *     @type string   $end       String to append to the end of the excerpt. Default '&hellip;'
442
     *                               (HTML ellipsis character).
443
     *     @type bool     $force     Whether to shorten the excerpt to the length/word count
444
     *                               specified, if the editor wrote a manual excerpt longer than the
445
     *                               set length. Default `false`.
446
     *     @type bool     $strip     Whether to strip HTML tags. Default `true`.
447
     *     @type string   $read_more String for what the "Read More" text should be. Default
448
     *                               'Read More'.
449
     * }
450
     * @example
451
     * ```twig
452
     * <h2>{{ post.title }}</h2>
453
     * <div>{{ post.excerpt({ words: 100, read_more: 'Keep reading' }) }}</div>
454
     * ```
455
     * @return PostExcerpt
456
     */
457
    public function excerpt(array $options = [])
38✔
458
    {
459
        return new PostExcerpt($this, $options);
38✔
460
    }
461

462
    /**
463
     * Gets an excerpt of your post.
464
     *
465
     * If you have an excerpt is set on the post, the excerpt will be used. Otherwise it will try to
466
     * pull from an excerpt from `post_content`. If there’s a `<!-- more -->` tag in the post
467
     * content, it will use that to mark where to pull through.
468
     *
469
     * This method returns a `Timber\PostExcerpt` object, which is a **chainable object**. This
470
     * means that you can change the output of the excerpt by **adding more methods**. Refer to the
471
     * [documentation of the `Timber\PostExcerpt` class](https://timber.github.io/docs/v2/reference/timber-postexcerpt/)
472
     * to get an overview of all the available methods.
473
     *
474
     * @api
475
     * @deprecated 2.0.0, use `{{ post.excerpt }}` instead.
476
     * @see PostExcerpt
477
     * @example
478
     * ```twig
479
     * {# Use default excerpt #}
480
     * <p>{{ post.excerpt }}</p>
481
     *
482
     * {# Change the post excerpt text #}
483
     * <p>{{ post.excerpt.read_more('Continue Reading') }}</p>
484
     *
485
     * {# Additionally restrict the length to 50 words #}
486
     * <p>{{ post.excerpt.length(50).read_more('Continue Reading') }}</p>
487
     * ```
488
     * @return PostExcerpt
489
     */
490
    public function preview()
×
491
    {
492
        Helper::deprecated('{{ post.preview }}', '{{ post.excerpt }}', '2.0.0');
×
493
        return new PostExcerpt($this);
×
494
    }
495

496
    /**
497
     * Gets the link to a page number.
498
     *
499
     * @internal
500
     * @param int $i
501
     * @return string|null Link to page number or `null` if link could not be read.
502
     */
503
    protected static function get_wp_link_page($i)
4✔
504
    {
505
        $link = \_wp_link_page($i);
4✔
506
        $link = new SimpleXMLElement($link . '</a>');
4✔
507

508
        return $link['href'] ?? null;
4✔
509
    }
510

511
    /**
512
     * Gets info to import on Timber post object.
513
     *
514
     * Used internally by init, etc. to build Timber\Post object.
515
     *
516
     * @internal
517
     *
518
     * @param array $data Data to update.
519
     * @return array
520
     */
521
    protected function get_info(array $data): array
435✔
522
    {
523
        $data = \array_merge($data, [
435✔
524
            'slug' => $this->wp_object->post_name,
435✔
525
            'status' => $this->wp_object->post_status,
435✔
526
        ]);
435✔
527

528
        return $data;
435✔
529
    }
530

531
    /**
532
     * Gets the comment form for use on a single article page
533
     *
534
     * @api
535
     * @param array $args see [WordPress docs on comment_form](https://codex.wordpress.org/Function_Reference/comment_form)
536
     *                    for reference on acceptable parameters
537
     * @return string of HTML for the form
538
     */
539
    public function comment_form($args = [])
1✔
540
    {
541
        return \trim(Helper::ob_function('comment_form', [$args, $this->ID]));
1✔
542
    }
543

544
    /**
545
     * Gets the terms associated with the post.
546
     *
547
     * @api
548
     * @example
549
     * ```twig
550
     * <section id="job-feed">
551
     * {% if jobs is not empty %}
552
     *   {% for post in jobs %}
553
     *       <div class="job">
554
     *           <h2>{{ post.title }}</h2>
555
     *           <p>{{ post.terms({
556
     *               taxonomy: 'category',
557
     *               orderby: 'name',
558
     *               order: 'ASC'
559
     *           })|join(', ') }}</p>
560
     *       </div>
561
     *   {% endfor %}
562
     * {% endif %}
563
     * </section>
564
     * ```
565
     * ```html
566
     * <section id="job-feed">
567
     *     <div class="job">
568
     *         <h2>Cheese Maker</h2>
569
     *         <p>Cheese, Food, Fromage</p>
570
     *     </div>
571
     *     <div class="job">
572
     *         <h2>Mime</h2>
573
     *         <p>Performance, Silence</p>
574
     *     </div>
575
     * </section>
576
     * ```
577
     * ```php
578
     * // Get all terms of a taxonomy.
579
     * $terms = $post->terms( 'category' );
580
     *
581
     * // Get terms of multiple taxonomies.
582
     * $terms = $post->terms( array( 'books', 'movies' ) );
583
     *
584
     * // Use custom arguments for taxonomy query and options.
585
     * $terms = $post->terms( [
586
     *     'taxonomy' => 'custom_tax',
587
     *     'orderby'  => 'count'
588
     * ], [
589
     *     'merge' => false
590
     * ] );
591
     * ```
592
     *
593
     * @param string|array $query_args     Any array of term query parameters for getting the terms.
594
     *                                  See `WP_Term_Query::__construct()` for supported arguments.
595
     *                                  Use the `taxonomy` argument to choose which taxonomies to
596
     *                                  get. Defaults to querying all registered taxonomies for the
597
     *                                  post type. You can use custom or built-in WordPress
598
     *                                  taxonomies (category, tag). Timber plays nice and figures
599
     *                                  out that `tag`, `tags` or `post_tag` are all the same
600
     *                                  (also for `categories` or `category`). For custom
601
     *                                  taxonomies you need to define the proper name.
602
     * @param array $options {
603
     *     Optional. An array of options for the function.
604
     *
605
     *     @type bool $merge Whether the resulting array should be one big one (`true`) or whether
606
     *                       it should be an array of sub-arrays for each taxonomy (`false`).
607
     *                       Default `true`.
608
     * }
609
     * @return array An array of taxonomies.
610
     */
611
    public function terms($query_args = [], $options = [])
15✔
612
    {
613
        // Make it possible to use a taxonomy or an array of taxonomies as a shorthand.
614
        if (!\is_array($query_args) || isset($query_args[0])) {
15✔
615
            $query_args = [
8✔
616
                'taxonomy' => $query_args,
8✔
617
            ];
8✔
618
        }
619

620
        /**
621
         * Handles backwards compatibility for users who use an array with a query property.
622
         *
623
         * @deprecated 2.0.0 use Post::terms( $query_args, $options )
624
         */
625
        if (\is_array($query_args) && isset($query_args['query'])) {
15✔
626
            if (isset($query_args['merge']) && !isset($options['merge'])) {
7✔
627
                $options['merge'] = $query_args['merge'];
3✔
628
            }
629
            $query_args = $query_args['query'];
7✔
630
        }
631

632
        // Defaults.
633
        $query_args = \wp_parse_args($query_args, [
15✔
634
            'taxonomy' => 'all',
15✔
635
        ]);
15✔
636

637
        $options = \wp_parse_args($options, [
15✔
638
            'merge' => true,
15✔
639
        ]);
15✔
640

641
        $taxonomies = $query_args['taxonomy'];
15✔
642
        $merge = $options['merge'];
15✔
643

644
        if (\in_array($taxonomies, ['all', 'any', ''])) {
15✔
645
            $taxonomies = \get_object_taxonomies($this->post_type);
3✔
646
        }
647

648
        if (!\is_array($taxonomies)) {
15✔
649
            $taxonomies = [$taxonomies];
12✔
650
        }
651

652
        $query = \array_merge($query_args, [
15✔
653
            'object_ids' => [$this->ID],
15✔
654
            'taxonomy' => $taxonomies,
15✔
655
        ]);
15✔
656

657
        if (!$merge) {
15✔
658
            // get results segmented out per taxonomy
659
            $queries = $this->partition_tax_queries($query, $taxonomies);
3✔
660
            $termGroups = Timber::get_terms($queries);
3✔
661

662
            // zip 'em up with the right keys
663
            return \array_combine($taxonomies, $termGroups);
3✔
664
        }
665

666
        return Timber::get_terms($query, $options);
13✔
667
    }
668

669
    /**
670
     * @api
671
     * @param string|int $term_name_or_id
672
     * @param string $taxonomy
673
     * @return bool
674
     */
675
    public function has_term($term_name_or_id, $taxonomy = 'all')
1✔
676
    {
677
        if ($taxonomy == 'all' || $taxonomy == 'any') {
1✔
678
            $taxes = \get_object_taxonomies($this->post_type, 'names');
1✔
679
            $ret = false;
1✔
680
            foreach ($taxes as $tax) {
1✔
681
                if (\has_term($term_name_or_id, $tax, $this->ID)) {
1✔
682
                    $ret = true;
1✔
683
                    break;
1✔
684
                }
685
            }
686
            return $ret;
1✔
687
        }
688
        return \has_term($term_name_or_id, $taxonomy, $this->ID);
1✔
689
    }
690

691
    /**
692
     * Gets the number of comments on a post.
693
     *
694
     * @api
695
     * @return int The number of comments on a post
696
     */
697
    public function comment_count(): int
2✔
698
    {
699
        return (int) \get_comments_number($this->ID);
2✔
700
    }
701

702
    /**
703
     * @api
704
     * @param string $field_name
705
     * @return boolean
706
     */
707
    public function has_field($field_name)
2✔
708
    {
709
        return (!$this->meta($field_name)) ? false : true;
2✔
710
    }
711

712
    /**
713
     * Gets the field object data from Advanced Custom Fields.
714
     * This includes metadata on the field like whether it's conditional or not.
715
     *
716
     * @api
717
     * @since 1.6.0
718
     * @param string $field_name of the field you want to lookup.
719
     * @return mixed
720
     */
721
    public function field_object($field_name)
1✔
722
    {
723
        /**
724
         * Filters field object data from Advanced Custom Fields.
725
         *
726
         * This filter is used by the ACF Integration.
727
         *
728
         * @see   \Timber\Post::field_object()
729
         * @since 1.6.0
730
         *
731
         * @param mixed        $value      The value.
732
         * @param int|null     $post_id    The post ID.
733
         * @param string       $field_name The ACF field name.
734
         * @param Post $post       The post object.
735
         */
736
        $value = \apply_filters('timber/post/meta_object_field', null, $this->ID, $field_name, $this);
1✔
737
        $value = $this->convert($value);
1✔
738
        return $value;
1✔
739
    }
740

741
    /**
742
     * @inheritDoc
743
     */
744
    protected function fetch_meta($field_name = '', $args = [], $apply_filters = true)
62✔
745
    {
746
        $revised_data = $this->get_revised_data_from_method('meta', $field_name);
62✔
747

748
        if ($revised_data) {
62✔
749
            return $revised_data;
×
750
        }
751

752
        return parent::fetch_meta($field_name, $args, $apply_filters);
62✔
753
    }
754

755
    /**
756
     * Gets a post meta value.
757
     *
758
     * @api
759
     * @deprecated 2.0.0, use `{{ post.meta('field_name') }}` instead.
760
     * @see \Timber\Post::meta()
761
     *
762
     * @param string $field_name The field name for which you want to get the value.
763
     * @return mixed The meta field value.
764
     */
765
    public function get_field($field_name = null)
1✔
766
    {
767
        Helper::deprecated(
1✔
768
            "{{ post.get_field('field_name') }}",
1✔
769
            "{{ post.meta('field_name') }}",
1✔
770
            '2.0.0'
1✔
771
        );
1✔
772

773
        if ($field_name === null) {
1✔
774
            // On the off-chance the field is actually named meta.
775
            $field_name = 'meta';
×
776
        }
777

778
        return $this->meta($field_name);
1✔
779
    }
780

781
    /**
782
     * Import field data onto this object
783
     *
784
     * @api
785
     * @deprecated since 2.0.0
786
     * @param string $field_name
787
     */
788
    public function import_field($field_name)
×
789
    {
790
        Helper::deprecated(
×
791
            "Importing field data onto an object",
×
792
            "{{ post.meta('field_name') }}",
×
793
            '2.0.0'
×
794
        );
×
795

796
        $this->$field_name = $this->meta($field_name);
×
797
    }
798

799
    /**
800
     * Get the CSS classes for a post without cache.
801
     * For usage you should use `{{post.class}}`
802
     *
803
     * @internal
804
     * @param string $class additional classes you want to add.
805
     * @example
806
     * ```twig
807
     * <article class="{{ post.post_class }}">
808
     *    {# Some stuff here #}
809
     * </article>
810
     * ```
811
     *
812
     * ```html
813
     * <article class="post-2612 post type-post status-publish format-standard has-post-thumbnail hentry category-data tag-charleston-church-shooting tag-dylann-roof tag-gun-violence tag-hate-crimes tag-national-incident-based-reporting-system">
814
     *    {# Some stuff here #}
815
     * </article>
816
     * ```
817
     * @return string a space-separated list of classes
818
     */
819
    public function post_class($class = '')
6✔
820
    {
821
        global $post;
822
        $old_global_post = $post;
6✔
823
        $post = $this;
6✔
824

825
        $class_array = \get_post_class($class, $this->ID);
6✔
826
        if (static::is_previewing()) {
6✔
827
            $class_array = \get_post_class($class, $this->post_parent);
1✔
828
        }
829
        $class_array = \implode(' ', $class_array);
6✔
830

831
        $post = $old_global_post;
6✔
832
        return $class_array;
6✔
833
    }
834

835
    /**
836
     * Get the CSS classes for a post, but with caching css post classes. For usage you should use `{{ post.class }}` instead of `{{post.css_class}}` or `{{post.post_class}}`
837
     *
838
     * @internal
839
     * @param string $class additional classes you want to add.
840
     * @see \Timber\Post::$_css_class
841
     * @example
842
     * ```twig
843
     * <article class="{{ post.class }}">
844
     *    {# Some stuff here #}
845
     * </article>
846
     * ```
847
     *
848
     * @return string a space-separated list of classes
849
     */
850
    public function css_class($class = '')
5✔
851
    {
852
        if (!$this->_css_class) {
5✔
853
            $this->_css_class = $this->post_class();
5✔
854
        }
855

856
        return \trim(\sprintf('%s %s', $this->_css_class, $class));
5✔
857
    }
858

859
    /**
860
     * @return array
861
     * @codeCoverageIgnore
862
     */
863
    public function get_method_values(): array
864
    {
865
        $ret['ancestors'] = $this->ancestors();
866
        $ret['author'] = $this->author();
867
        $ret['categories'] = $this->categories();
868
        $ret['category'] = $this->category();
869
        $ret['children'] = $this->children();
870
        $ret['comments'] = $this->comments();
871
        $ret['content'] = $this->content();
872
        $ret['edit_link'] = $this->edit_link();
873
        $ret['format'] = $this->format();
874
        $ret['link'] = $this->link();
875
        $ret['next'] = $this->next();
876
        $ret['pagination'] = $this->pagination();
877
        $ret['parent'] = $this->parent();
878
        $ret['path'] = $this->path();
879
        $ret['prev'] = $this->prev();
880
        $ret['terms'] = $this->terms();
881
        $ret['tags'] = $this->tags();
882
        $ret['thumbnail'] = $this->thumbnail();
883
        $ret['title'] = $this->title();
884
        return $ret;
885
    }
886

887
    /**
888
     * Returns an array of ancestors of the post as Timber\Posts
889
     * (or other class as you define).
890
     *
891
     * @api
892
     * @example
893
     * ```twig
894
     * {% if post.ancestors is not empty %}
895
     *     Here are the ancestor pages:
896
     *     {% for ancestor in post.ancestors %}
897
     *         <a href="{{ ancestor.link }}">{{ ancestor.title }}</a>
898
     *     {% endfor %}
899
     * {% endif %}
900
     * ```
901
     * @return PostCollectionInterface
902
     */
903
    public function ancestors()
6✔
904
    {
905
        if (isset($this->_ancestors)) {
6✔
906
            return $this->_ancestors;
×
907
        }
908

909
        $ancestors = \array_reverse(\get_post_ancestors($this->ID));
6✔
910
        $this->_ancestors = $this->factory()->from($ancestors);
6✔
911

912
        return $this->_ancestors;
6✔
913
    }
914

915
    /**
916
     * Return the author of a post
917
     *
918
     * @api
919
     * @example
920
     * ```twig
921
     * <h1>{{post.title}}</h1>
922
     * <p class="byline">
923
     *     <a href="{{post.author.link}}">{{post.author.name}}</a>
924
     * </p>
925
     * ```
926
     * @return User|null A User object if found, false if not
927
     */
928
    public function author()
10✔
929
    {
930
        if (isset($this->post_author)) {
10✔
931
            $factory = new UserFactory();
10✔
932
            return $factory->from((int) $this->post_author);
10✔
933
        }
934
    }
935

936
    /**
937
     * Got more than one author? That's cool, but you'll need Co-Authors plus or another plugin to access any data
938
     *
939
     * @api
940
     * @return array
941
     */
942
    public function authors()
5✔
943
    {
944
        /**
945
         * Filters authors for a post.
946
         *
947
         * This filter is used by the CoAuthorsPlus integration.
948
         *
949
         * @example
950
         * ```
951
         * add_filter( 'timber/post/authors', function( $author, $post ) {
952
         *      foreach ($cauthors as $author) {
953
         *        // do something with $author
954
         *      }
955
         *
956
         *     return $authors;
957
         * } );
958
         * ```
959
         *
960
         * @see   \Timber\Post::authors()
961
         * @since 1.1.4
962
         *
963
         * @param array        $authors An array of User objects. Default: User object for `post_author`.
964
         * @param Post $post    The post object.
965
         */
966
        return \apply_filters('timber/post/authors', [$this->author()], $this);
5✔
967
    }
968

969
    /**
970
     * Get the author (WordPress user) who last modified the post
971
     *
972
     * @api
973
     * @example
974
     * ```twig
975
     * Last updated by {{ post.modified_author.name }}
976
     * ```
977
     * ```html
978
     * Last updated by Harper Lee
979
     * ```
980
     * @return User|null A User object if found, false if not
981
     */
982
    public function modified_author()
1✔
983
    {
984
        $user_id = \get_post_meta($this->ID, '_edit_last', true);
1✔
985
        return ($user_id ? Timber::get_user($user_id) : $this->author());
1✔
986
    }
987

988
    /**
989
     * Get the categories on a particular post
990
     *
991
     * @api
992
     * @return array of Timber\Term objects
993
     */
994
    public function categories()
5✔
995
    {
996
        return $this->terms('category');
5✔
997
    }
998

999
    /**
1000
     * Gets a category attached to a post.
1001
     *
1002
     * If multiple categories are set, it will return just the first one.
1003
     *
1004
     * @api
1005
     * @return Term|null
1006
     */
1007
    public function category()
2✔
1008
    {
1009
        $cats = $this->categories();
2✔
1010
        if (\count($cats) && isset($cats[0])) {
2✔
1011
            return $cats[0];
2✔
1012
        }
1013

1014
        return null;
×
1015
    }
1016

1017
    /**
1018
     * Returns an array of children on the post as Timber\Posts
1019
     * (or other claass as you define).
1020
     *
1021
     * @api
1022
     * @example
1023
     * ```twig
1024
     * {% if post.children is not empty %}
1025
     *     Here are the child pages:
1026
     *     {% for child in post.children %}
1027
     *         <a href="{{ child.link }}">{{ child.title }}</a>
1028
     *     {% endfor %}
1029
     * {% endif %}
1030
     * ```
1031
     * @param string|array $args _optional_ An array of arguments for the `get_children` function or a string/non-indexed array to use as the post type(s).
1032
     * @return PostCollectionInterface
1033
     */
1034
    public function children($args = 'any')
5✔
1035
    {
1036
        if (\is_string($args) || \array_values($args) === $args) {
5✔
1037
            $args = [
4✔
1038
                'post_type' => 'parent' === $args ? $this->post_type : $args,
4✔
1039
            ];
4✔
1040
        }
1041

1042
        $args = \wp_parse_args($args, [
5✔
1043
            'post_parent' => $this->ID,
5✔
1044
            'post_type' => 'any',
5✔
1045
            'posts_per_page' => -1,
5✔
1046
            'orderby' => 'menu_order title',
5✔
1047
            'order' => 'ASC',
5✔
1048
            'post_status' => 'publish' === $this->post_status ? ['publish', 'inherit'] : 'publish',
5✔
1049
        ]);
5✔
1050

1051
        /**
1052
         * Filters the arguments for the query used to get the children of a post.
1053
         *
1054
         * This filter is used by the `Timber\Post::children()` method. It allows you to modify the
1055
         * arguments for the `get_children` function. This way you can change the query to get the
1056
         * children of a post.
1057
         *
1058
         * @example
1059
         * ```
1060
         * add_filter( 'timber/post/children_args', function( $args, $post ) {
1061
         *
1062
         *     if ( $post->post_type === 'custom_post_type' ) {
1063
         *        $args['post_status'] = 'private';
1064
         *     }
1065
         *
1066
         *     return $args;
1067
         * } );
1068
         * ```
1069
         *
1070
         * @see   \Timber\Post::children()
1071
         * @since 2.1.0
1072
         *
1073
         * @param array        $arguments An array of arguments for the `get_children` function.
1074
         * @param Post $post   The post object.
1075
         */
1076
        $args = \apply_filters('timber/post/children_args', $args, $this);
5✔
1077

1078
        return $this->factory()->from(\get_children($args));
5✔
1079
    }
1080

1081
    /**
1082
     * Gets the comments on a Timber\Post and returns them as an array of `Timber\Comment` objects (or whatever comment class you set).
1083
     *
1084
     * @api
1085
     * Gets the comments on a `Timber\Post` and returns them as a `Timber\CommentThread`: a PHP
1086
     * ArrayObject of [`Timber\Comment`](https://timber.github.io/docs/v2/reference/timber-comment/)
1087
     * (or whatever comment class you set).
1088
     * @api
1089
     *
1090
     * @param int    $count        Set the number of comments you want to get. `0` is analogous to
1091
     *                             "all".
1092
     * @param string $order        Use ordering set in WordPress admin, or a different scheme.
1093
     * @param string $type         For when other plugins use the comments table for their own
1094
     *                             special purposes. Might be set to 'liveblog' or other, depending
1095
     *                             on what’s stored in your comments table.
1096
     * @param string $status       Could be 'pending', etc.
1097
     * @see CommentThread for an example with nested comments
1098
     * @return bool|CommentThread
1099
     *
1100
     * @example
1101
     *
1102
     * **single.twig**
1103
     *
1104
     * ```twig
1105
     * <div id="post-comments">
1106
     *   <h4>Comments on {{ post.title }}</h4>
1107
     *   <ul>
1108
     *     {% for comment in post.comments() %}
1109
     *       {% include 'comment.twig' %}
1110
     *     {% endfor %}
1111
     *   </ul>
1112
     *   <div class="comment-form">
1113
     *     {{ function('comment_form') }}
1114
     *   </div>
1115
     * </div>
1116
     * ```
1117
     *
1118
     * **comment.twig**
1119
     *
1120
     * ```twig
1121
     * {# comment.twig #}
1122
     * <li>
1123
     *   <p class="comment-author">{{ comment.author.name }} says:</p>
1124
     *   <div>{{ comment.content }}</div>
1125
     * </li>
1126
     * ```
1127
     */
1128
    public function comments($count = null, $order = 'wp', $type = 'comment', $status = 'approve')
13✔
1129
    {
1130
        global $overridden_cpage, $user_ID;
1131
        $overridden_cpage = false;
13✔
1132

1133
        $commenter = \wp_get_current_commenter();
13✔
1134
        $comment_author_email = $commenter['comment_author_email'];
13✔
1135

1136
        $args = [
13✔
1137
            'status' => $status,
13✔
1138
            'order' => $order,
13✔
1139
            'type' => $type,
13✔
1140
        ];
13✔
1141
        if ($count > 0) {
13✔
1142
            $args['number'] = $count;
1✔
1143
        }
1144
        if (\strtolower($order) == 'wp' || \strtolower($order) == 'wordpress') {
13✔
1145
            $args['order'] = \get_option('comment_order');
13✔
1146
        }
1147
        if ($user_ID) {
13✔
1148
            $args['include_unapproved'] = [$user_ID];
1✔
1149
        } elseif (!empty($comment_author_email)) {
13✔
1150
            $args['include_unapproved'] = [$comment_author_email];
1✔
1151
        } elseif (\function_exists('wp_get_unapproved_comment_author_email')) {
12✔
1152
            $unapproved_email = \wp_get_unapproved_comment_author_email();
12✔
1153
            if ($unapproved_email) {
12✔
1154
                $args['include_unapproved'] = [$unapproved_email];
1✔
1155
            }
1156
        }
1157
        $ct = new CommentThread($this->ID, false);
13✔
1158
        $ct->init($args);
13✔
1159
        return $ct;
13✔
1160
    }
1161

1162
    /**
1163
     * If the Password form is to be shown, show it!
1164
     * @return string|void
1165
     */
1166
    protected function maybe_show_password_form()
38✔
1167
    {
1168
        if ($this->password_required()) {
38✔
1169
            $show_pw = false;
3✔
1170

1171
            /**
1172
             * Filters whether the password form should be shown for password protected posts.
1173
             *
1174
             * This filter runs only when you call `{{ post.content }}` for a password protected
1175
             * post. When this filter returns `true`, a password form will be shown instead of the
1176
             * post content. If you want to modify the form itself, you can use the
1177
             * `timber/post/content/password_form` filter.
1178
             *
1179
             * @since 1.1.4
1180
             * @example
1181
             * ```php
1182
             * // Always show password form for password protected posts.
1183
             * add_filter( 'timber/post/content/show_password_form_for_protected', '__return_true' );
1184
             * ```
1185
             *
1186
             * @param bool $show_pw Whether the password form should be shown. Default `false`.
1187
             */
1188
            $show_pw = \apply_filters('timber/post/content/show_password_form_for_protected', $show_pw);
3✔
1189

1190
            if ($show_pw) {
3✔
1191
                /**
1192
                 * Filters the password form output.
1193
                 *
1194
                 * As an alternative to this filter, you could also use WordPress’s `the_password_form` filter.
1195
                 * The difference to this filter is, that you’ll also have the post object available as a second
1196
                 * parameter, in case you need that.
1197
                 *
1198
                 * @since 1.1.4
1199
                 *
1200
                 * @example
1201
                 * ```php
1202
                 * // Modify the password form.
1203
                 * add_filter( 'timber/post/content/password_form', function( $form, $post ) {
1204
                 *     return Timber::compile( 'assets/password-form.twig', array( 'post' => $post ) );
1205
                 * }, 10, 2 );
1206
                 * ```
1207
                 *
1208
                 * @param string       $form Form output. Default WordPress password form output generated by `get_the_password_form()`.
1209
                 * @param Post $post The post object.
1210
                 */
1211
                return \apply_filters('timber/post/content/password_form', \get_the_password_form($this->ID), $this);
2✔
1212
            }
1213
        }
1214
    }
1215

1216
    /**
1217
     *
1218
     */
1219
    protected function get_revised_data_from_method($method, $args = false)
147✔
1220
    {
1221
        if (!\is_array($args)) {
147✔
1222
            $args = [$args];
110✔
1223
        }
1224
        $rev = $this->get_post_preview_object();
147✔
1225
        if ($rev && $this->ID == $rev->post_parent && $this->ID != $rev->ID) {
147✔
1226
            return \call_user_func_array([$rev, $method], $args);
8✔
1227
        }
1228
    }
1229

1230
    /**
1231
     * Gets the actual content of a WordPress post.
1232
     *
1233
     * As opposed to using `{{ post.post_content }}`, this will run the hooks/filters attached to
1234
     * the `the_content` filter. It will return your post’s content with WordPress filters run on it
1235
     * – which means it will parse blocks, convert shortcodes or run `wpautop()` on the content.
1236
     *
1237
     * If you use page breaks in your content to split your post content into multiple pages,
1238
     * use `{{ post.paged_content }}` to display only the content for the current page.
1239
     *
1240
     * @api
1241
     * @example
1242
     * ```twig
1243
     * <article>
1244
     *     <h1>{{ post.title }}</h1>
1245
     *
1246
     *     <div class="content">{{ post.content }}</div>
1247
     * </article>
1248
     * ```
1249
     *
1250
     * @param int $page Optional. The page to show if the content of the post is split into multiple
1251
     *                  pages. Read more about this in the [Pagination Guide](https://timber.github.io/docs/v2/guides/pagination/#paged-content-within-a-post). Default `0`.
1252
     * @param int $len Optional. The number of words to show. Default `-1` (show all).
1253
     * @param bool $remove_blocks Optional. Whether to remove blocks. Defaults to false. True when called from the $post->excerpt() method.
1254
     * @return string The content of the post.
1255
     */
1256
    public function content($page = 0, $len = -1, $remove_blocks = false)
38✔
1257
    {
1258
        if ($rd = $this->get_revised_data_from_method('content', [$page, $len])) {
38✔
1259
            return $rd;
4✔
1260
        }
1261
        if ($form = $this->maybe_show_password_form()) {
38✔
1262
            return $form;
2✔
1263
        }
1264
        if ($len == -1 && $page == 0 && $this->___content) {
36✔
1265
            return $this->___content;
1✔
1266
        }
1267

1268
        $content = $this->post_content;
36✔
1269

1270
        if ($len > 0) {
36✔
1271
            $content = \wp_trim_words($content, $len);
1✔
1272
        }
1273

1274
        /**
1275
         * Page content split by <!--nextpage-->.
1276
         *
1277
         * @see WP_Query::generate_postdata()
1278
         */
1279
        if ($page && \str_contains((string) $content, '<!--nextpage-->')) {
36✔
1280
            $content = \str_replace("\n<!--nextpage-->\n", '<!--nextpage-->', (string) $content);
6✔
1281
            $content = \str_replace("\n<!--nextpage-->", '<!--nextpage-->', $content);
6✔
1282
            $content = \str_replace("<!--nextpage-->\n", '<!--nextpage-->', $content);
6✔
1283

1284
            // Remove the nextpage block delimiters, to avoid invalid block structures in the split content.
1285
            $content = \str_replace('<!-- wp:nextpage -->', '', $content);
6✔
1286
            $content = \str_replace('<!-- /wp:nextpage -->', '', $content);
6✔
1287

1288
            // Ignore nextpage at the beginning of the content.
1289
            if (\str_starts_with($content, '<!--nextpage-->')) {
6✔
1290
                $content = \substr($content, 15);
1✔
1291
            }
1292

1293
            $pages = \explode('<!--nextpage-->', $content);
6✔
1294
            $page--;
6✔
1295

1296
            if (\count($pages) > $page) {
6✔
1297
                $content = $pages[$page];
6✔
1298
            }
1299
        }
1300

1301
        /**
1302
         * Filters whether the content produced by block editor blocks should be removed or not from the content.
1303
         *
1304
         * If truthy then block whose content does not belong in the excerpt, will be removed.
1305
         * This removal is done using WordPress Core `excerpt_remove_blocks` function.
1306
         *
1307
         * @since 2.1.1
1308
         *
1309
         * @param bool $remove_blocks Whether blocks whose content should not be part of the excerpt should be removed
1310
         *                            or not from the excerpt.
1311
         *
1312
         * @see   excerpt_remove_blocks() The WordPress Core function that will handle the block removal from the excerpt.
1313
         */
1314
        $remove_blocks = (bool) \apply_filters('timber/post/content/remove_blocks', $remove_blocks);
36✔
1315

1316
        if ($remove_blocks) {
36✔
1317
            $content = \excerpt_remove_blocks($content);
17✔
1318
        }
1319

1320
        $content = $this->content_handle_no_teaser_block($content);
36✔
1321
        $content = \apply_filters('the_content', ($content));
36✔
1322

1323
        if ($len == -1 && $page == 0) {
36✔
1324
            $this->___content = $content;
35✔
1325
        }
1326

1327
        return $content;
36✔
1328
    }
1329

1330
    /**
1331
     * Handles for an circumstance with the Block editor where a "more" block has an option to
1332
     * "Hide the excerpt on the full content page" which hides everything prior to the inserted
1333
     * "more" block
1334
     * @ticket #2218
1335
     * @param string $content
1336
     * @return string
1337
     */
1338
    protected function content_handle_no_teaser_block($content)
36✔
1339
    {
1340
        if ((\str_contains($content, 'noTeaser:true') || \str_contains($content, '"noTeaser":true')) && \str_contains($content, '<!-- /wp:more -->')) {
36✔
1341
            $arr = \explode('<!-- /wp:more -->', $content);
1✔
1342
            return \trim($arr[1]);
1✔
1343
        }
1344
        return $content;
35✔
1345
    }
1346

1347
    /**
1348
     * Gets the paged content for a post.
1349
     *
1350
     * You will use this, if you use `<!--nextpage-->` in your post content or the Page Break block
1351
     * in the Block Editor. Use `{{ post.pagination }}` to create a pagination for your paged
1352
     * content. Learn more about this in the [Pagination Guide](https://timber.github.io/docs/v2/guides/pagination/#paged-content-within-a-post).
1353
     *
1354
     * @example
1355
     * ```twig
1356
     * {{ post.paged_content }}
1357
     * ```
1358
     *
1359
     * @return string The content for the current page. If there’s no page break found in the
1360
     *                content, the whole content is returned.
1361
     */
1362
    public function paged_content()
4✔
1363
    {
1364
        global $page;
1365
        return $this->content($page, -1);
4✔
1366
    }
1367

1368
    /**
1369
     * Gets the timestamp when the post was published.
1370
     *
1371
     * @api
1372
     * @since 2.0.0
1373
     *
1374
     * @return false|int Unix timestamp on success, false on failure.
1375
     */
1376
    public function timestamp()
10✔
1377
    {
1378
        return \get_post_timestamp($this->ID);
10✔
1379
    }
1380

1381
    /**
1382
     * Gets the timestamp when the post was last modified.
1383
     *
1384
     * @api
1385
     * @since 2.0.0
1386
     *
1387
     * @return false|int Unix timestamp on success, false on failure.
1388
     */
1389
    public function modified_timestamp()
4✔
1390
    {
1391
        return \get_post_timestamp($this->ID, 'modified');
4✔
1392
    }
1393

1394
    /**
1395
     * Gets the publishing date of the post.
1396
     *
1397
     * This function will also apply the
1398
     * [`get_the_date`](https://developer.wordpress.org/reference/hooks/get_the_date/) filter to the
1399
     * output.
1400
     *
1401
     * If you use {{ post.date }} with the |time_ago filter, then make sure that you use a time
1402
     * format including the full time and not just the date.
1403
     *
1404
     * @api
1405
     * @example
1406
     * ```twig
1407
     * {# Uses date format set in Settings → General #}
1408
     * Published on {{ post.date }}
1409
     * OR
1410
     * Published on {{ post.date('F jS') }}
1411
     * which was
1412
     * {{ post.date('U')|time_ago }}
1413
     * {{ post.date('Y-m-d H:i:s')|time_ago }}
1414
     * {{ post.date(constant('DATE_ATOM'))|time_ago }}
1415
     * ```
1416
     *
1417
     * ```html
1418
     * Published on January 12, 2015
1419
     * OR
1420
     * Published on Jan 12th
1421
     * which was
1422
     * 8 years ago
1423
     * ```
1424
     *
1425
     * @param string|null $date_format Optional. PHP date format. Will use the `date_format` option
1426
     *                                 as a default.
1427
     *
1428
     * @return string
1429
     */
1430
    public function date($date_format = null)
8✔
1431
    {
1432
        $format = $date_format ?: \get_option('date_format');
8✔
1433
        $date = \wp_date($format, $this->timestamp());
8✔
1434

1435
        /**
1436
         * Filters the date a post was published.
1437
         *
1438
         * @see get_the_date()
1439
         *
1440
         * @param string      $date        The formatted date.
1441
         * @param string      $date_format PHP date format. Defaults to 'date_format' option if not
1442
         *                                 specified.
1443
         * @param int|WP_Post $id          The post object or ID.
1444
         */
1445
        $date = \apply_filters('get_the_date', $date, $date_format, $this->ID);
8✔
1446

1447
        return $date;
8✔
1448
    }
1449

1450
    /**
1451
     * Gets the date the post was last modified.
1452
     *
1453
     * This function will also apply the
1454
     * [`get_the_modified_date`](https://developer.wordpress.org/reference/hooks/get_the_modified_date/)
1455
     * filter to the output.
1456
     *
1457
     * @api
1458
     * @example
1459
     * ```twig
1460
     * {# Uses date format set in Settings → General #}
1461
     * Last modified on {{ post.modified_date }}
1462
     * OR
1463
     * Last modified on {{ post.modified_date('F jS') }}
1464
     * ```
1465
     *
1466
     * ```html
1467
     * Last modified on January 12, 2015
1468
     * OR
1469
     * Last modified on Jan 12th
1470
     * ```
1471
     *
1472
     * @param string|null $date_format Optional. PHP date format. Will use the `date_format` option
1473
     *                                 as a default.
1474
     *
1475
     * @return string
1476
     */
1477
    public function modified_date($date_format = null)
2✔
1478
    {
1479
        $format = $date_format ?: \get_option('date_format');
2✔
1480
        $date = \wp_date($format, $this->modified_timestamp());
2✔
1481

1482
        /**
1483
         * Filters the date a post was last modified.
1484
         *
1485
         * This filter expects a `WP_Post` object as the last parameter. We only have a
1486
         * `Timber\Post` object available, that wouldn’t match the expected argument. That’s why we
1487
         * need to get the post object with get_post(). This is fairly inexpensive, because the post
1488
         * will already be in the cache.
1489
         *
1490
         * @see get_the_modified_date()
1491
         *
1492
         * @param string|bool  $date        The formatted date or false if no post is found.
1493
         * @param string       $date_format PHP date format. Defaults to value specified in
1494
         *                                  'date_format' option.
1495
         * @param WP_Post|null $post        WP_Post object or null if no post is found.
1496
         */
1497
        $date = \apply_filters('get_the_modified_date', $date, $date_format, \get_post($this->ID));
2✔
1498

1499
        return $date;
2✔
1500
    }
1501

1502
    /**
1503
     * Gets the time the post was published to use in your template.
1504
     *
1505
     * This function will also apply the
1506
     * [`get_the_time`](https://developer.wordpress.org/reference/hooks/get_the_time/) filter to the
1507
     * output.
1508
     *
1509
     * @api
1510
     * @example
1511
     * ```twig
1512
     * {# Uses time format set in Settings → General #}
1513
     * Published at {{ post.time }}
1514
     * OR
1515
     * Published at {{ post.time('G:i') }}
1516
     * ```
1517
     *
1518
     * ```html
1519
     * Published at 1:25 pm
1520
     * OR
1521
     * Published at 13:25
1522
     * ```
1523
     *
1524
     * @param string|null $time_format Optional. PHP date format. Will use the `time_format` option
1525
     *                                 as a default.
1526
     *
1527
     * @return string
1528
     */
1529
    public function time($time_format = null)
2✔
1530
    {
1531
        $format = $time_format ?: \get_option('time_format');
2✔
1532
        $time = \wp_date($format, $this->timestamp());
2✔
1533

1534
        /**
1535
         * Filters the time a post was written.
1536
         *
1537
         * @see get_the_time()
1538
         *
1539
         * @param string      $time        The formatted time.
1540
         * @param string      $time_format Format to use for retrieving the time the post was
1541
         *                                 written. Accepts 'G', 'U', or php date format value
1542
         *                                 specified in `time_format` option. Default empty.
1543
         * @param int|WP_Post $id          WP_Post object or ID.
1544
         */
1545
        $time = \apply_filters('get_the_time', $time, $time_format, $this->ID);
2✔
1546

1547
        return $time;
2✔
1548
    }
1549

1550
    /**
1551
     * Gets the time of the last modification of the post to use in your template.
1552
     *
1553
     * This function will also apply the
1554
     * [`get_the_time`](https://developer.wordpress.org/reference/hooks/get_the_modified_time/)
1555
     * filter to the output.
1556
     *
1557
     * @api
1558
     * @example
1559
     * ```twig
1560
     * {# Uses time format set in Settings → General #}
1561
     * Published at {{ post.modified_time }}
1562
     * OR
1563
     * Published at {{ post.modified_time('G:i') }}
1564
     * ```
1565
     *
1566
     * ```html
1567
     * Published at 1:25 pm
1568
     * OR
1569
     * Published at 13:25
1570
     * ```
1571
     *
1572
     * @param string|null $time_format Optional. PHP date format. Will use the `time_format` option
1573
     *                                 as a default.
1574
     *
1575
     * @return string
1576
     */
1577
    public function modified_time($time_format = null)
2✔
1578
    {
1579
        $format = $time_format ?: \get_option('time_format');
2✔
1580
        $time = \wp_date($format, $this->modified_timestamp());
2✔
1581

1582
        /**
1583
         * Filters the localized time a post was last modified.
1584
         *
1585
         * This filter expects a `WP_Post` object as the last parameter. We only have a
1586
         * `Timber\Post` object available, that wouldn’t match the expected argument. That’s why we
1587
         * need to get the post object with get_post(). This is fairly inexpensive, because the post
1588
         * will already be in the cache.
1589
         *
1590
         * @see get_the_modified_time()
1591
         *
1592
         * @param string|bool  $time        The formatted time or false if no post is found.
1593
         * @param string       $time_format Format to use for retrieving the time the post was
1594
         *                                  written. Accepts 'G', 'U', or php date format. Defaults
1595
         *                                  to value specified in 'time_format' option.
1596
         * @param WP_Post|null $post        WP_Post object or null if no post is found.
1597
         */
1598
        $time = \apply_filters('get_the_modified_time', $time, $time_format, \get_post($this->ID));
2✔
1599

1600
        return $time;
2✔
1601
    }
1602

1603
    /**
1604
     * Returns the PostType object for a post’s post type with labels and other info.
1605
     *
1606
     * @api
1607
     * @since 1.0.4
1608
     * @example
1609
     * ```twig
1610
     * This post is from <span>{{ post.type.labels.name }}</span>
1611
     * ```
1612
     *
1613
     * ```html
1614
     * This post is from <span>Recipes</span>
1615
     * ```
1616
     * @return PostType
1617
     */
1618
    public function type()
3✔
1619
    {
1620
        if (!$this->__type instanceof PostType) {
3✔
1621
            $this->__type = new PostType($this->post_type);
3✔
1622
        }
1623
        return $this->__type;
3✔
1624
    }
1625

1626
    /**
1627
     * Checks whether the current user can edit the post.
1628
     *
1629
     * @api
1630
     * @example
1631
     * ```twig
1632
     * {% if post.can_edit %}
1633
     *     <a href="{{ post.edit_link }}">Edit</a>
1634
     * {% endif %}
1635
     * ```
1636
     * @return bool
1637
     */
1638
    public function can_edit(): bool
2✔
1639
    {
1640
        return \current_user_can('edit_post', $this->ID);
2✔
1641
    }
1642

1643
    /**
1644
     * Gets the edit link for a post if the current user has the correct rights.
1645
     *
1646
     * @api
1647
     * @example
1648
     * ```twig
1649
     * {% if post.can_edit %}
1650
     *     <a href="{{ post.edit_link }}">Edit</a>
1651
     * {% endif %}
1652
     * ```
1653
     * @return string|null The edit URL of a post in the WordPress admin or null if the current user can’t edit the
1654
     *                     post.
1655
     */
1656
    public function edit_link(): ?string
1✔
1657
    {
1658
        if (!$this->can_edit()) {
1✔
1659
            return null;
1✔
1660
        }
1661

1662
        return \get_edit_post_link($this->ID);
1✔
1663
    }
1664

1665
    /**
1666
     * @api
1667
     * @return mixed
1668
     */
1669
    public function format()
1✔
1670
    {
1671
        return \get_post_format($this->ID);
1✔
1672
    }
1673

1674
    /**
1675
     * whether post requires password and correct password has been provided
1676
     * @api
1677
     * @return boolean
1678
     */
1679
    public function password_required()
39✔
1680
    {
1681
        return \post_password_required($this->ID);
39✔
1682
    }
1683

1684
    /**
1685
     * get the permalink for a post object
1686
     * @api
1687
     * @example
1688
     * ```twig
1689
     * <a href="{{post.link}}">Read my post</a>
1690
     * ```
1691
     * @return string ex: https://example.org/2015/07/my-awesome-post
1692
     */
1693
    public function link()
32✔
1694
    {
1695
        if (isset($this->_permalink)) {
32✔
1696
            return $this->_permalink;
14✔
1697
        }
1698
        $this->_permalink = \get_permalink($this->ID);
32✔
1699
        return $this->_permalink;
32✔
1700
    }
1701

1702
    /**
1703
     * @api
1704
     * @return string
1705
     */
1706
    public function name()
1✔
1707
    {
1708
        return $this->title();
1✔
1709
    }
1710

1711
    /**
1712
     * Gets the next post that is adjacent to the current post in a collection.
1713
     *
1714
     * Works pretty much the same as
1715
     * [`get_next_post()`](https://developer.wordpress.org/reference/functions/get_next_post/).
1716
     *
1717
     * @api
1718
     * @example
1719
     * ```twig
1720
     * {% if post.next %}
1721
     *     <a href="{{ post.next.link }}">{{ post.next.title }}</a>
1722
     * {% endif %}
1723
     * ```
1724
     * @param bool|string $in_same_term Whether the post should be in a same taxonomy term. Default
1725
     *                                  `false`.
1726
     *
1727
     * @return mixed
1728
     */
1729
    public function next($in_same_term = false)
5✔
1730
    {
1731
        if (!isset($this->_next) || !isset($this->_next[$in_same_term])) {
5✔
1732
            global $post;
1733
            $this->_next = [];
5✔
1734
            $old_global = $post;
5✔
1735
            $post = $this;
5✔
1736
            if (\is_string($in_same_term) && \strlen($in_same_term)) {
5✔
1737
                $adjacent = \get_adjacent_post(true, '', false, $in_same_term);
2✔
1738
            } else {
1739
                $adjacent = \get_adjacent_post(false, '', false);
3✔
1740
            }
1741

1742
            if ($adjacent) {
5✔
1743
                $this->_next[$in_same_term] = $this->factory()->from($adjacent);
4✔
1744
            } else {
1745
                $this->_next[$in_same_term] = false;
1✔
1746
            }
1747
            $post = $old_global;
5✔
1748
        }
1749
        return $this->_next[$in_same_term];
5✔
1750
    }
1751

1752
    /**
1753
     * Gets a data array to display a pagination for your paginated post.
1754
     *
1755
     * Use this in combination with `{{ post.paged_content }}`.
1756
     *
1757
     * @api
1758
     * @example
1759
     * Using simple links to the next an previous page.
1760
     * ```twig
1761
     * {% if post.pagination.next is not empty %}
1762
     *     <a href="{{ post.pagination.next.link|esc_url }}">Go to next page</a>
1763
     * {% endif %}
1764
     *
1765
     * {% if post.pagination.prev is not empty %}
1766
     *     <a href="{{ post.pagination.prev.link|esc_url }}">Go to previous page</a>
1767
     * {% endif %}
1768
     * ```
1769
     * Using a pagination for all pages.
1770
     * ```twig
1771
     * {% if post.pagination.pages is not empty %}
1772
     *    <nav aria-label="pagination">
1773
     *        <ul>
1774
     *            {% for page in post.pagination.pages %}
1775
     *                <li>
1776
     *                    {% if page.current %}
1777
     *                        <span aria-current="page">Page {{ page.title }}</span>
1778
     *                    {% else %}
1779
     *                        <a href="{{ page.link|esc_ur }}">Page {{ page.title }}</a>
1780
     *                    {% endif %}
1781
     *                </li>
1782
     *            {% endfor %}
1783
     *        </ul>
1784
     *    </nav>
1785
     * {% endif %}
1786
     * ```
1787
     *
1788
     * @return array An array with data to build your paginated content.
1789
     */
1790
    public function pagination()
4✔
1791
    {
1792
        global $post, $page, $numpages, $multipage;
1793
        $post = $this;
4✔
1794
        $ret = [];
4✔
1795
        if ($multipage) {
4✔
1796
            for ($i = 1; $i <= $numpages; $i++) {
4✔
1797
                $link = self::get_wp_link_page($i);
4✔
1798
                $data = [
4✔
1799
                    'name' => $i,
4✔
1800
                    'title' => $i,
4✔
1801
                    'text' => $i,
4✔
1802
                    'link' => $link,
4✔
1803
                ];
4✔
1804
                if ($i == $page) {
4✔
1805
                    $data['current'] = true;
4✔
1806
                }
1807
                $ret['pages'][] = $data;
4✔
1808
            }
1809
            $i = $page - 1;
4✔
1810
            if ($i) {
4✔
1811
                $link = self::get_wp_link_page($i);
×
1812
                $ret['prev'] = [
×
1813
                    'link' => $link,
×
1814
                ];
×
1815
            }
1816
            $i = $page + 1;
4✔
1817
            if ($i <= $numpages) {
4✔
1818
                $link = self::get_wp_link_page($i);
4✔
1819
                $ret['next'] = [
4✔
1820
                    'link' => $link,
4✔
1821
                ];
4✔
1822
            }
1823
        }
1824
        return $ret;
4✔
1825
    }
1826

1827
    /**
1828
     * Finds any WP_Post objects and converts them to Timber\Post objects.
1829
     *
1830
     * @api
1831
     * @param array|WP_Post $data
1832
     */
1833
    public function convert($data)
17✔
1834
    {
1835
        if (\is_object($data)) {
17✔
1836
            $data = Helper::convert_wp_object($data);
15✔
1837
        } elseif (\is_array($data)) {
7✔
1838
            $data = \array_map([$this, 'convert'], $data);
6✔
1839
        }
1840
        return $data;
17✔
1841
    }
1842

1843
    /**
1844
     * Gets the parent (if one exists) from a post as a Timber\Post object.
1845
     * Honors Class Maps.
1846
     *
1847
     * @api
1848
     * @example
1849
     * ```twig
1850
     * Parent page: <a href="{{ post.parent.link }}">{{ post.parent.title }}</a>
1851
     * ```
1852
     * @return bool|Post
1853
     */
1854
    public function parent()
3✔
1855
    {
1856
        if (!$this->post_parent) {
3✔
1857
            return false;
1✔
1858
        }
1859

1860
        return $this->factory()->from($this->post_parent);
2✔
1861
    }
1862

1863
    /**
1864
     * Gets the relative path of a WP Post, so while link() will return https://example.org/2015/07/my-cool-post
1865
     * this will return just /2015/07/my-cool-post
1866
     *
1867
     * @api
1868
     * @example
1869
     * ```twig
1870
     * <a href="{{post.path}}">{{post.title}}</a>
1871
     * ```
1872
     * @return string
1873
     */
1874
    public function path()
3✔
1875
    {
1876
        return URLHelper::get_rel_url($this->link());
3✔
1877
    }
1878

1879
    /**
1880
     * Get the previous post that is adjacent to the current post in a collection.
1881
     *
1882
     * Works pretty much the same as
1883
     * [`get_previous_post()`](https://developer.wordpress.org/reference/functions/get_previous_post/).
1884
     *
1885
     * @api
1886
     * @example
1887
     * ```twig
1888
     * {% if post.prev %}
1889
     *     <a href="{{ post.prev.link }}">{{ post.prev.title }}</a>
1890
     * {% endif %}
1891
     * ```
1892
     * @param bool|string $in_same_term Whether the post should be in a same taxonomy term. Default
1893
     *                                  `false`.
1894
     * @return mixed
1895
     */
1896
    public function prev($in_same_term = false)
3✔
1897
    {
1898
        if (isset($this->_prev) && isset($this->_prev[$in_same_term])) {
3✔
1899
            return $this->_prev[$in_same_term];
×
1900
        }
1901
        global $post;
1902
        $old_global = $post;
3✔
1903
        $post = $this;
3✔
1904
        $within_taxonomy = $in_same_term ?: 'category';
3✔
1905
        $adjacent = \get_adjacent_post(($in_same_term), '', true, $within_taxonomy);
3✔
1906
        $prev_in_taxonomy = false;
3✔
1907
        if ($adjacent) {
3✔
1908
            $prev_in_taxonomy = $this->factory()->from($adjacent);
3✔
1909
        }
1910
        $this->_prev[$in_same_term] = $prev_in_taxonomy;
3✔
1911
        $post = $old_global;
3✔
1912
        return $this->_prev[$in_same_term];
3✔
1913
    }
1914

1915
    /**
1916
     * Gets the tags on a post, uses WP's post_tag taxonomy
1917
     *
1918
     * @api
1919
     * @return array
1920
     */
1921
    public function tags()
3✔
1922
    {
1923
        return $this->terms('post_tag');
3✔
1924
    }
1925

1926
    /**
1927
     * Gets the post’s thumbnail ID.
1928
     *
1929
     * @api
1930
     * @since 2.0.0
1931
     *
1932
     * @return false|int The default post’s ID. False if no thumbnail was defined.
1933
     */
1934
    public function thumbnail_id()
25✔
1935
    {
1936
        return (int) \get_post_meta($this->ID, '_thumbnail_id', true);
25✔
1937
    }
1938

1939
    /**
1940
     * get the featured image as a Timber/Image
1941
     *
1942
     * @api
1943
     * @example
1944
     * ```twig
1945
     * <img src="{{ post.thumbnail.src }}" />
1946
     * ```
1947
     * @return Image|null of your thumbnail
1948
     */
1949
    public function thumbnail()
24✔
1950
    {
1951
        $tid = $this->thumbnail_id();
24✔
1952

1953
        if ($tid) {
24✔
1954
            return $this->factory()->from($tid);
22✔
1955
        }
1956

1957
        return null;
2✔
1958
    }
1959

1960
    /**
1961
     * Returns the processed title to be used in templates. This returns the title of the post after WP's filters have run. This is analogous to `the_title()` in standard WP template tags.
1962
     *
1963
     * @api
1964
     * @example
1965
     * ```twig
1966
     * <h1>{{ post.title }}</h1>
1967
     * ```
1968
     * @return string
1969
     */
1970
    public function title()
54✔
1971
    {
1972
        if ($rd = $this->get_revised_data_from_method('title')) {
54✔
1973
            return $rd;
1✔
1974
        }
1975
        return \apply_filters('the_title', $this->post_title, $this->ID);
54✔
1976
    }
1977

1978
    /**
1979
     * Returns galleries from the post’s content.
1980
     *
1981
     * @api
1982
     * @example
1983
     * ```twig
1984
     * {{ post.gallery }}
1985
     * ```
1986
     * @return array A list of arrays, each containing gallery data and srcs parsed from the
1987
     * expanded shortcode.
1988
     */
1989
    public function gallery($html = true)
1✔
1990
    {
1991
        $galleries = \get_post_galleries($this->ID, $html);
1✔
1992
        $gallery = \reset($galleries);
1✔
1993

1994
        return \apply_filters('get_post_gallery', $gallery, $this->ID, $galleries);
1✔
1995
    }
1996

1997
    protected function get_entity_name()
×
1998
    {
1999
        return 'post';
×
2000
    }
2001

2002
    /**
2003
     * Given a base query and a list of taxonomies, return a list of queries
2004
     * each of which queries for one of the taxonomies.
2005
     * @example
2006
     * ```
2007
     * $this->partition_tax_queries(["object_ids" => [123]], ["a", "b"]);
2008
     *
2009
     * // result:
2010
     * // [
2011
     * //   ["object_ids" => [123], "taxonomy" => ["a"]],
2012
     * //   ["object_ids" => [123], "taxonomy" => ["b"]],
2013
     * // ]
2014
     * ```
2015
     * @internal
2016
     */
2017
    private function partition_tax_queries(array $query, array $taxonomies): array
3✔
2018
    {
2019
        return \array_map(fn (string $tax): array => \array_merge($query, [
3✔
2020
            'taxonomy' => [$tax],
3✔
2021
        ]), $taxonomies);
3✔
2022
    }
2023

2024
    /**
2025
     * Get a PostFactory instance for internal usage
2026
     *
2027
     * @internal
2028
     * @return PostFactory
2029
     */
2030
    private function factory()
42✔
2031
    {
2032
        static $factory;
42✔
2033
        $factory = $factory ?: new PostFactory();
42✔
2034
        return $factory;
42✔
2035
    }
2036
}
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