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

timber / timber / 23149188281

16 Mar 2026 02:36PM UTC coverage: 89.606% (-0.02%) from 89.626%
23149188281

push

travis-ci

web-flow
fix: "Fix content cache poisoning when excerpt() is called first (#3208)

* fix: "Fix content cache poisoning when excerpt() is called first (#3208)
* fix: Update assertion to check if content ends with expected string in TimberPostContentTest, fixes trunk test

1 of 1 new or added line in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

4595 of 5128 relevant lines covered (89.61%)

63.76 hits per line

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

94.47
/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
     * The post's slug.
174
     *
175
     * @var string
176
     */
177
    public $post_name;
178

179
    /**
180
     * The post's GMT publication time.
181
     *
182
     * @var string
183
     */
184
    public $post_date_gmt;
185

186
    /**
187
     * Whether comments are allowed.
188
     *
189
     * @var string
190
     */
191
    public $comment_status;
192

193
    /**
194
     * Whether pings are allowed.
195
     *
196
     * @var string
197
     */
198
    public $ping_status;
199

200
    /**
201
     * The post's password in plain text.
202
     *
203
     * @var string
204
     */
205
    public $post_password;
206

207
    /**
208
     * URLs queued to be pinged.
209
     *
210
     * @var string
211
     */
212
    public $to_ping;
213

214
    /**
215
     * URLs that have been pinged.
216
     *
217
     * @var string
218
     */
219
    public $pinged;
220

221
    /**
222
     * The post's local modified time.
223
     *
224
     * @var string
225
     */
226
    public $post_modified;
227

228
    /**
229
     * The post's GMT modified time.
230
     *
231
     * @var string
232
     */
233
    public $post_modified_gmt;
234

235
    /**
236
     * A utility DB field for post content.
237
     *
238
     * @var string
239
     */
240
    public $post_content_filtered;
241

242
    /**
243
     * The unique identifier for a post, not necessarily a URL, used as the feed GUID.
244
     *
245
     * @var string
246
     */
247
    public $guid;
248

249
    /**
250
     * A field used for ordering posts.
251
     *
252
     * @var int
253
     */
254
    public $menu_order;
255

256
    /**
257
     * An attachment's mime type.
258
     *
259
     * @since 3.5.0
260
     * @var string
261
     */
262
    public $post_mime_type;
263

264
    /**
265
     * Cached comment count.
266
     *
267
     * A numeric string, for compatibility reasons.
268
     *
269
     * @var string
270
     */
271
    public $comment_count;
272

273
    /**
274
     * Stores the post object's sanitization level.
275
     *
276
     * Does not correspond to a DB field.
277
     *
278
     * @var string
279
     */
280
    public $filter;
281

282
    /**
283
     * @var string Stores the PostType object for the post.
284
     */
285
    protected $__type;
286

287
    /**
288
     * Create and initialize a new instance of the called Post class
289
     * (i.e. Timber\Post or a subclass).
290
     *
291
     * @internal
292
     * @return static
293
     */
294
    public static function build(WP_Post $wp_post): static
438✔
295
    {
296
        $post = new static();
438✔
297

298
        $post->id = $wp_post->ID;
438✔
299
        $post->ID = $wp_post->ID;
438✔
300
        $post->wp_object = $wp_post;
438✔
301

302
        $data = \get_object_vars($wp_post);
438✔
303
        $data = $post->get_info($data);
438✔
304

305
        /**
306
         * Filters the imported post data.
307
         *
308
         * Used internally for previews.
309
         *
310
         * @since 2.0.0
311
         * @see   Timber::init()
312
         * @param array        $data An array of post data to import.
313
         * @param Post $post The Timber post instance.
314
         */
315
        $data = \apply_filters('timber/post/import_data', $data, $post);
438✔
316

317
        $post->import($data);
438✔
318

319
        return $post;
438✔
320
    }
321

322
    /**
323
     * If you send the constructor nothing it will try to figure out the current post id based on
324
     * being inside The_Loop.
325
     *
326
     * @internal
327
     */
328
    protected function __construct()
438✔
329
    {
330
    }
438✔
331

332
    /**
333
     * This is helpful for twig to return properties and methods see:
334
     * https://github.com/fabpot/Twig/issues/2
335
     *
336
     * This is also here to ensure that {{ post.class }} remains usable.
337
     *
338
     * @api
339
     *
340
     * @return mixed
341
     */
342
    public function __get($field)
22✔
343
    {
344
        if ('class' === $field) {
22✔
345
            return $this->css_class();
1✔
346
        }
347

348
        if ('_thumbnail_id' === $field) {
21✔
349
            Helper::doing_it_wrong(
2✔
350
                "Accessing the thumbnail ID through {{ {$this->object_type}._thumbnail_id }}",
2✔
351
                "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✔
352
                '2.0.0'
2✔
353
            );
2✔
354
        }
355

356
        return parent::__get($field);
21✔
357
    }
358

359
    /**
360
     * This is helpful for twig to return properties and methods see:
361
     * https://github.com/fabpot/Twig/issues/2
362
     *
363
     * This is also here to ensure that {{ post.class }} remains usable
364
     *
365
     * @api
366
     *
367
     * @return mixed
368
     */
369
    public function __call($field, $args)
10✔
370
    {
371
        if ('class' === $field) {
10✔
372
            $class = $args[0] ?? '';
3✔
373
            return $this->css_class($class);
3✔
374
        }
375

376
        return parent::__call($field, $args);
7✔
377
    }
378

379
    /**
380
     * Gets the underlying WordPress Core object.
381
     *
382
     * @since 2.0.0
383
     *
384
     * @return WP_Post|null
385
     */
386
    public function wp_object(): ?WP_Post
1✔
387
    {
388
        return $this->wp_object;
1✔
389
    }
390

391
    /**
392
     * Sets up a post.
393
     *
394
     * Sets up the `$post` global, and other global variables as well as variables in the
395
     * `$wp_query` global that makes Timber more compatible with WordPress.
396
     *
397
     * This function will be called automatically when you loop over Timber posts as well as in
398
     * `Timber::context()`.
399
     *
400
     * @api
401
     * @since 2.0.0
402
     *
403
     * @return Post The post instance.
404
     */
405
    public function setup()
33✔
406
    {
407
        global $post;
408
        global $wp_query;
409

410
        // Mimic WordPress behavior to improve compatibility with third party plugins.
411
        $wp_query->in_the_loop = true;
33✔
412

413
        if (!$this->wp_object) {
33✔
414
            return $this;
×
415
        }
416

417
        /**
418
         * Maybe set or overwrite post global.
419
         *
420
         * We have to overwrite the post global to be compatible with a couple of WordPress plugins
421
         * that work with the post global in certain conditions.
422
         */
423
        // phpcs:ignore WordPress.WP.GlobalVariablesOverride.OverrideProhibited
424
        if (!$post || isset($post->ID) && $post->ID !== $this->ID) {
33✔
425
            $post = $this->wp_object;
22✔
426
        }
427

428
        // The setup_postdata() function will call the 'the_post' action.
429
        $wp_query->setup_postdata($this->wp_object);
33✔
430

431
        return $this;
33✔
432
    }
433

434
    /**
435
     * Resets variables after post has been used.
436
     *
437
     * This function will be called automatically when you loop over Timber posts.
438
     *
439
     * @api
440
     * @since 2.0.0
441
     *
442
     * @return Post The post instance.
443
     */
444
    public function teardown()
22✔
445
    {
446
        global $wp_query;
447

448
        $wp_query->in_the_loop = false;
22✔
449

450
        return $this;
22✔
451
    }
452

453
    /**
454
     * Determine whether or not an admin/editor is looking at the post in "preview mode" via the
455
     * WordPress admin
456
     * @internal
457
     * @return bool
458
     */
459
    protected static function is_previewing()
155✔
460
    {
461
        global $wp_query;
462
        return isset($_GET['preview']) && isset($_GET['preview_nonce']) && \wp_verify_nonce($_GET['preview_nonce'], 'post_preview_' . $wp_query->queried_object_id);
155✔
463
    }
464

465
    /**
466
     * Outputs the title of the post if you do something like `<h1>{{post}}</h1>`
467
     *
468
     * @api
469
     * @return string
470
     */
471
    public function __toString()
1✔
472
    {
473
        return $this->title();
1✔
474
    }
475

476
    protected function get_post_preview_object()
149✔
477
    {
478
        global $wp_query;
479
        if (static::is_previewing()) {
149✔
480
            $revision_id = $this->get_post_preview_id($wp_query);
9✔
481
            return Timber::get_post($revision_id);
9✔
482
        }
483
    }
484

485
    protected function get_post_preview_id($query)
9✔
486
    {
487
        $can = [
9✔
488
            \get_post_type_object($query->queried_object->post_type)->cap->edit_post,
9✔
489
        ];
9✔
490

491
        if ($query->queried_object->author_id !== \get_current_user_id()) {
9✔
492
            $can[] = \get_post_type_object($query->queried_object->post_type)->cap->edit_others_posts;
9✔
493
        }
494

495
        $can_preview = [];
9✔
496

497
        foreach ($can as $type) {
9✔
498
            if (\current_user_can($type, $query->queried_object_id)) {
9✔
499
                $can_preview[] = true;
8✔
500
            }
501
        }
502

503
        if (\count($can_preview) !== \count($can)) {
9✔
504
            return;
1✔
505
        }
506

507
        $revisions = \wp_get_post_revisions($query->queried_object_id);
8✔
508

509
        if (!empty($revisions)) {
8✔
510
            $revision = \reset($revisions);
8✔
511
            return $revision->ID;
8✔
512
        }
513

514
        return false;
×
515
    }
516

517
    /**
518
     * Updates post_meta of the current object with the given value.
519
     *
520
     * @deprecated 2.0.0 Use `update_post_meta()` instead.
521
     *
522
     * @param string $field The key of the meta field to update.
523
     * @param mixed  $value The new value.
524
     */
525
    public function update($field, $value)
1✔
526
    {
527
        Helper::deprecated('Timber\Post::update()', 'update_post_meta()', '2.0.0');
1✔
528

529
        if (isset($this->ID)) {
1✔
530
            \update_post_meta($this->ID, $field, $value);
1✔
531
            $this->$field = $value;
1✔
532
        }
533
    }
534

535
    /**
536
     * Gets a excerpt of your post.
537
     *
538
     * If you have an excerpt is set on the post, the excerpt will be used. Otherwise it will try to
539
     * pull from an excerpt from `post_content`. If there’s a `<!-- more -->` tag in the post
540
     * content, it will use that to mark where to pull through.
541
     *
542
     * @api
543
     * @see PostExcerpt
544
     *
545
     * @param array $options {
546
     *     An array of configuration options for generating the excerpt. Default empty.
547
     *
548
     *     @type int      $words     Number of words in the excerpt. Default `50`.
549
     *     @type int|bool $chars     Number of characters in the excerpt. Default `false` (no
550
     *                               character limit).
551
     *     @type string   $end       String to append to the end of the excerpt. Default '&hellip;'
552
     *                               (HTML ellipsis character).
553
     *     @type bool     $force     Whether to shorten the excerpt to the length/word count
554
     *                               specified, if the editor wrote a manual excerpt longer than the
555
     *                               set length. Default `false`.
556
     *     @type bool     $strip     Whether to strip HTML tags. Default `true`.
557
     *     @type string   $read_more String for what the "Read More" text should be. Default
558
     *                               'Read More'.
559
     * }
560
     * @example
561
     * ```twig
562
     * <h2>{{ post.title }}</h2>
563
     * <div>{{ post.excerpt({ words: 100, read_more: 'Keep reading' }) }}</div>
564
     * ```
565
     * @return PostExcerpt
566
     */
567
    public function excerpt(array $options = [])
39✔
568
    {
569
        return new PostExcerpt($this, $options);
39✔
570
    }
571

572
    /**
573
     * Gets an excerpt of your post.
574
     *
575
     * If you have an excerpt is set on the post, the excerpt will be used. Otherwise it will try to
576
     * pull from an excerpt from `post_content`. If there’s a `<!-- more -->` tag in the post
577
     * content, it will use that to mark where to pull through.
578
     *
579
     * This method returns a `Timber\PostExcerpt` object, which is a **chainable object**. This
580
     * means that you can change the output of the excerpt by **adding more methods**. Refer to the
581
     * [documentation of the `Timber\PostExcerpt` class](https://timber.github.io/docs/v2/reference/timber-postexcerpt/)
582
     * to get an overview of all the available methods.
583
     *
584
     * @api
585
     * @deprecated 2.0.0, use `{{ post.excerpt }}` instead.
586
     * @see PostExcerpt
587
     * @example
588
     * ```twig
589
     * {# Use default excerpt #}
590
     * <p>{{ post.excerpt }}</p>
591
     *
592
     * {# Change the post excerpt text #}
593
     * <p>{{ post.excerpt.read_more('Continue Reading') }}</p>
594
     *
595
     * {# Additionally restrict the length to 50 words #}
596
     * <p>{{ post.excerpt.length(50).read_more('Continue Reading') }}</p>
597
     * ```
598
     * @return PostExcerpt
599
     */
600
    public function preview()
×
601
    {
602
        Helper::deprecated('{{ post.preview }}', '{{ post.excerpt }}', '2.0.0');
×
603
        return new PostExcerpt($this);
×
604
    }
605

606
    /**
607
     * Gets the link to a page number.
608
     *
609
     * @internal
610
     * @param int $i
611
     * @return string|null Link to page number or `null` if link could not be read.
612
     */
613
    protected static function get_wp_link_page($i)
4✔
614
    {
615
        $link = \_wp_link_page($i);
4✔
616
        $link = new SimpleXMLElement($link . '</a>');
4✔
617

618
        return $link['href'] ?? null;
4✔
619
    }
620

621
    /**
622
     * Gets info to import on Timber post object.
623
     *
624
     * Used internally by init, etc. to build Timber\Post object.
625
     *
626
     * @internal
627
     *
628
     * @param array $data Data to update.
629
     * @return array
630
     */
631
    protected function get_info(array $data): array
438✔
632
    {
633
        $data = \array_merge($data, [
438✔
634
            'slug' => $this->wp_object->post_name,
438✔
635
            'status' => $this->wp_object->post_status,
438✔
636
        ]);
438✔
637

638
        return $data;
438✔
639
    }
640

641
    /**
642
     * Gets the comment form for use on a single article page
643
     *
644
     * @api
645
     * @param array $args see [WordPress docs on comment_form](https://codex.wordpress.org/Function_Reference/comment_form)
646
     *                    for reference on acceptable parameters
647
     * @return string of HTML for the form
648
     */
649
    public function comment_form($args = [])
1✔
650
    {
651
        return \trim(Helper::ob_function('comment_form', [$args, $this->ID]));
1✔
652
    }
653

654
    /**
655
     * Gets the terms associated with the post.
656
     *
657
     * @api
658
     * @example
659
     * ```twig
660
     * <section id="job-feed">
661
     * {% if jobs is not empty %}
662
     *   {% for post in jobs %}
663
     *       <div class="job">
664
     *           <h2>{{ post.title }}</h2>
665
     *           <p>{{ post.terms({
666
     *               taxonomy: 'category',
667
     *               orderby: 'name',
668
     *               order: 'ASC'
669
     *           })|join(', ') }}</p>
670
     *       </div>
671
     *   {% endfor %}
672
     * {% endif %}
673
     * </section>
674
     * ```
675
     * ```html
676
     * <section id="job-feed">
677
     *     <div class="job">
678
     *         <h2>Cheese Maker</h2>
679
     *         <p>Cheese, Food, Fromage</p>
680
     *     </div>
681
     *     <div class="job">
682
     *         <h2>Mime</h2>
683
     *         <p>Performance, Silence</p>
684
     *     </div>
685
     * </section>
686
     * ```
687
     * ```php
688
     * // Get all terms of a taxonomy.
689
     * $terms = $post->terms( 'category' );
690
     *
691
     * // Get terms of multiple taxonomies.
692
     * $terms = $post->terms( array( 'books', 'movies' ) );
693
     *
694
     * // Use custom arguments for taxonomy query and options.
695
     * $terms = $post->terms( [
696
     *     'taxonomy' => 'custom_tax',
697
     *     'orderby'  => 'count'
698
     * ], [
699
     *     'merge' => false
700
     * ] );
701
     * ```
702
     *
703
     * @param string|array $query_args     Any array of term query parameters for getting the terms.
704
     *                                  See `WP_Term_Query::__construct()` for supported arguments.
705
     *                                  Use the `taxonomy` argument to choose which taxonomies to
706
     *                                  get. Defaults to querying all registered taxonomies for the
707
     *                                  post type. You can use custom or built-in WordPress
708
     *                                  taxonomies (category, tag). Timber plays nice and figures
709
     *                                  out that `tag`, `tags` or `post_tag` are all the same
710
     *                                  (also for `categories` or `category`). For custom
711
     *                                  taxonomies you need to define the proper name.
712
     * @param array $options {
713
     *     Optional. An array of options for the function.
714
     *
715
     *     @type bool $merge Whether the resulting array should be one big one (`true`) or whether
716
     *                       it should be an array of sub-arrays for each taxonomy (`false`).
717
     *                       Default `true`.
718
     * }
719
     * @return array An array of taxonomies.
720
     */
721
    public function terms($query_args = [], $options = [])
15✔
722
    {
723
        // Make it possible to use a taxonomy or an array of taxonomies as a shorthand.
724
        if (!\is_array($query_args) || isset($query_args[0])) {
15✔
725
            $query_args = [
8✔
726
                'taxonomy' => $query_args,
8✔
727
            ];
8✔
728
        }
729

730
        /**
731
         * Handles backwards compatibility for users who use an array with a query property.
732
         *
733
         * @deprecated 2.0.0 use Post::terms( $query_args, $options )
734
         */
735
        if (\is_array($query_args) && isset($query_args['query'])) {
15✔
736
            if (isset($query_args['merge']) && !isset($options['merge'])) {
7✔
737
                $options['merge'] = $query_args['merge'];
3✔
738
            }
739
            $query_args = $query_args['query'];
7✔
740
        }
741

742
        // Defaults.
743
        $query_args = \wp_parse_args($query_args, [
15✔
744
            'taxonomy' => 'all',
15✔
745
        ]);
15✔
746

747
        $options = \wp_parse_args($options, [
15✔
748
            'merge' => true,
15✔
749
        ]);
15✔
750

751
        $taxonomies = $query_args['taxonomy'];
15✔
752
        $merge = $options['merge'];
15✔
753

754
        if (\in_array($taxonomies, ['all', 'any', ''])) {
15✔
755
            $taxonomies = \get_object_taxonomies($this->post_type);
3✔
756
        }
757

758
        if (!\is_array($taxonomies)) {
15✔
759
            $taxonomies = [$taxonomies];
12✔
760
        }
761

762
        $query = \array_merge($query_args, [
15✔
763
            'object_ids' => [$this->ID],
15✔
764
            'taxonomy' => $taxonomies,
15✔
765
        ]);
15✔
766

767
        if (!$merge) {
15✔
768
            // get results segmented out per taxonomy
769
            $queries = $this->partition_tax_queries($query, $taxonomies);
3✔
770
            $termGroups = Timber::get_terms($queries);
3✔
771

772
            // zip 'em up with the right keys
773
            return \array_combine($taxonomies, $termGroups);
3✔
774
        }
775

776
        return Timber::get_terms($query, $options);
13✔
777
    }
778

779
    /**
780
     * @api
781
     * @param string|int $term_name_or_id
782
     * @param string $taxonomy
783
     * @return bool
784
     */
785
    public function has_term($term_name_or_id, $taxonomy = 'all')
1✔
786
    {
787
        if ($taxonomy == 'all' || $taxonomy == 'any') {
1✔
788
            $taxes = \get_object_taxonomies($this->post_type, 'names');
1✔
789
            $ret = false;
1✔
790
            foreach ($taxes as $tax) {
1✔
791
                if (\has_term($term_name_or_id, $tax, $this->ID)) {
1✔
792
                    $ret = true;
1✔
793
                    break;
1✔
794
                }
795
            }
796
            return $ret;
1✔
797
        }
798
        return \has_term($term_name_or_id, $taxonomy, $this->ID);
1✔
799
    }
800

801
    /**
802
     * Gets the number of comments on a post.
803
     *
804
     * @api
805
     * @return int The number of comments on a post
806
     */
807
    public function comment_count(): int
2✔
808
    {
809
        return (int) \get_comments_number($this->ID);
2✔
810
    }
811

812
    /**
813
     * @api
814
     * @param string $field_name
815
     * @return boolean
816
     */
817
    public function has_field($field_name)
2✔
818
    {
819
        return (!$this->meta($field_name)) ? false : true;
2✔
820
    }
821

822
    /**
823
     * Gets the field object data from Advanced Custom Fields.
824
     * This includes metadata on the field like whether it's conditional or not.
825
     *
826
     * @api
827
     * @since 1.6.0
828
     * @param string $field_name of the field you want to lookup.
829
     * @return mixed
830
     */
831
    public function field_object($field_name)
1✔
832
    {
833
        /**
834
         * Filters field object data from Advanced Custom Fields.
835
         *
836
         * This filter is used by the ACF Integration.
837
         *
838
         * @see   \Timber\Post::field_object()
839
         * @since 1.6.0
840
         *
841
         * @param mixed        $value      The value.
842
         * @param int|null     $post_id    The post ID.
843
         * @param string       $field_name The ACF field name.
844
         * @param Post $post       The post object.
845
         */
846
        $value = \apply_filters('timber/post/meta_object_field', null, $this->ID, $field_name, $this);
1✔
847
        $value = $this->convert($value);
1✔
848
        return $value;
1✔
849
    }
850

851
    /**
852
     * @inheritDoc
853
     */
854
    protected function fetch_meta($field_name = '', $args = [], $apply_filters = true)
62✔
855
    {
856
        $revised_data = $this->get_revised_data_from_method('meta', $field_name);
62✔
857

858
        if ($revised_data) {
62✔
859
            return $revised_data;
×
860
        }
861

862
        return parent::fetch_meta($field_name, $args, $apply_filters);
62✔
863
    }
864

865
    /**
866
     * Gets a post meta value.
867
     *
868
     * @api
869
     * @deprecated 2.0.0, use `{{ post.meta('field_name') }}` instead.
870
     * @see \Timber\Post::meta()
871
     *
872
     * @param string $field_name The field name for which you want to get the value.
873
     * @return mixed The meta field value.
874
     */
875
    public function get_field($field_name = null)
1✔
876
    {
877
        Helper::deprecated(
1✔
878
            "{{ post.get_field('field_name') }}",
1✔
879
            "{{ post.meta('field_name') }}",
1✔
880
            '2.0.0'
1✔
881
        );
1✔
882

883
        if ($field_name === null) {
1✔
884
            // On the off-chance the field is actually named meta.
885
            $field_name = 'meta';
×
886
        }
887

888
        return $this->meta($field_name);
1✔
889
    }
890

891
    /**
892
     * Import field data onto this object
893
     *
894
     * @api
895
     * @deprecated since 2.0.0
896
     * @param string $field_name
897
     */
898
    public function import_field($field_name)
×
899
    {
900
        Helper::deprecated(
×
901
            "Importing field data onto an object",
×
902
            "{{ post.meta('field_name') }}",
×
903
            '2.0.0'
×
904
        );
×
905

906
        $this->$field_name = $this->meta($field_name);
×
907
    }
908

909
    /**
910
     * Get the CSS classes for a post without cache.
911
     * For usage you should use `{{post.class}}`
912
     *
913
     * @internal
914
     * @param string $class additional classes you want to add.
915
     * @example
916
     * ```twig
917
     * <article class="{{ post.post_class }}">
918
     *    {# Some stuff here #}
919
     * </article>
920
     * ```
921
     *
922
     * ```html
923
     * <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">
924
     *    {# Some stuff here #}
925
     * </article>
926
     * ```
927
     * @return string a space-separated list of classes
928
     */
929
    public function post_class($class = '')
6✔
930
    {
931
        global $post;
932
        $old_global_post = $post;
6✔
933
        $post = $this;
6✔
934

935
        $class_array = \get_post_class($class, $this->ID);
6✔
936
        if (static::is_previewing()) {
6✔
937
            $class_array = \get_post_class($class, $this->post_parent);
1✔
938
        }
939
        $class_array = \implode(' ', $class_array);
6✔
940

941
        $post = $old_global_post;
6✔
942
        return $class_array;
6✔
943
    }
944

945
    /**
946
     * 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}}`
947
     *
948
     * @internal
949
     * @param string $class additional classes you want to add.
950
     * @see \Timber\Post::$_css_class
951
     * @example
952
     * ```twig
953
     * <article class="{{ post.class }}">
954
     *    {# Some stuff here #}
955
     * </article>
956
     * ```
957
     *
958
     * @return string a space-separated list of classes
959
     */
960
    public function css_class($class = '')
5✔
961
    {
962
        if (!$this->_css_class) {
5✔
963
            $this->_css_class = $this->post_class();
5✔
964
        }
965

966
        return \trim(\sprintf('%s %s', $this->_css_class, $class));
5✔
967
    }
968

969
    /**
970
     * @return array
971
     * @codeCoverageIgnore
972
     */
973
    public function get_method_values(): array
974
    {
975
        $ret['ancestors'] = $this->ancestors();
976
        $ret['author'] = $this->author();
977
        $ret['categories'] = $this->categories();
978
        $ret['category'] = $this->category();
979
        $ret['children'] = $this->children();
980
        $ret['comments'] = $this->comments();
981
        $ret['content'] = $this->content();
982
        $ret['edit_link'] = $this->edit_link();
983
        $ret['format'] = $this->format();
984
        $ret['link'] = $this->link();
985
        $ret['next'] = $this->next();
986
        $ret['pagination'] = $this->pagination();
987
        $ret['parent'] = $this->parent();
988
        $ret['path'] = $this->path();
989
        $ret['prev'] = $this->prev();
990
        $ret['terms'] = $this->terms();
991
        $ret['tags'] = $this->tags();
992
        $ret['thumbnail'] = $this->thumbnail();
993
        $ret['title'] = $this->title();
994
        return $ret;
995
    }
996

997
    /**
998
     * Returns an array of ancestors of the post as Timber\Posts
999
     * (or other class as you define).
1000
     *
1001
     * @api
1002
     * @example
1003
     * ```twig
1004
     * {% if post.ancestors is not empty %}
1005
     *     Here are the ancestor pages:
1006
     *     {% for ancestor in post.ancestors %}
1007
     *         <a href="{{ ancestor.link }}">{{ ancestor.title }}</a>
1008
     *     {% endfor %}
1009
     * {% endif %}
1010
     * ```
1011
     * @return PostCollectionInterface
1012
     */
1013
    public function ancestors()
7✔
1014
    {
1015
        if (isset($this->_ancestors)) {
7✔
1016
            return $this->_ancestors;
1✔
1017
        }
1018

1019
        $ancestors = $this->factory()->from(\array_reverse(\get_post_ancestors($this->ID)));
7✔
1020

1021
        return $this->_ancestors = \is_iterable($ancestors) ? $ancestors : new PostArrayObject([]);
7✔
1022
    }
1023

1024
    /**
1025
     * Return the author of a post
1026
     *
1027
     * @api
1028
     * @example
1029
     * ```twig
1030
     * <h1>{{post.title}}</h1>
1031
     * <p class="byline">
1032
     *     <a href="{{post.author.link}}">{{post.author.name}}</a>
1033
     * </p>
1034
     * ```
1035
     * @return User|null A User object if found, false if not
1036
     */
1037
    public function author()
10✔
1038
    {
1039
        if (isset($this->post_author)) {
10✔
1040
            $factory = new UserFactory();
10✔
1041
            return $factory->from((int) $this->post_author);
10✔
1042
        }
1043
    }
1044

1045
    /**
1046
     * Got more than one author? That's cool, but you'll need Co-Authors plus or another plugin to access any data
1047
     *
1048
     * @api
1049
     * @return array
1050
     */
1051
    public function authors()
5✔
1052
    {
1053
        /**
1054
         * Filters authors for a post.
1055
         *
1056
         * This filter is used by the CoAuthorsPlus integration.
1057
         *
1058
         * @example
1059
         * ```
1060
         * add_filter( 'timber/post/authors', function( $author, $post ) {
1061
         *      foreach ($cauthors as $author) {
1062
         *        // do something with $author
1063
         *      }
1064
         *
1065
         *     return $authors;
1066
         * } );
1067
         * ```
1068
         *
1069
         * @see   \Timber\Post::authors()
1070
         * @since 1.1.4
1071
         *
1072
         * @param array        $authors An array of User objects. Default: User object for `post_author`.
1073
         * @param Post $post    The post object.
1074
         */
1075
        return \apply_filters('timber/post/authors', [$this->author()], $this);
5✔
1076
    }
1077

1078
    /**
1079
     * Get the author (WordPress user) who last modified the post
1080
     *
1081
     * @api
1082
     * @example
1083
     * ```twig
1084
     * Last updated by {{ post.modified_author.name }}
1085
     * ```
1086
     * ```html
1087
     * Last updated by Harper Lee
1088
     * ```
1089
     * @return User|null A User object if found, false if not
1090
     */
1091
    public function modified_author()
1✔
1092
    {
1093
        $user_id = \get_post_meta($this->ID, '_edit_last', true);
1✔
1094
        return ($user_id ? Timber::get_user($user_id) : $this->author());
1✔
1095
    }
1096

1097
    /**
1098
     * Get the categories on a particular post
1099
     *
1100
     * @api
1101
     * @return array of Timber\Term objects
1102
     */
1103
    public function categories()
5✔
1104
    {
1105
        return $this->terms('category');
5✔
1106
    }
1107

1108
    /**
1109
     * Gets a category attached to a post.
1110
     *
1111
     * If multiple categories are set, it will return just the first one.
1112
     *
1113
     * @api
1114
     * @return Term|null
1115
     */
1116
    public function category()
2✔
1117
    {
1118
        $cats = $this->categories();
2✔
1119
        if (\count($cats) && isset($cats[0])) {
2✔
1120
            return $cats[0];
2✔
1121
        }
1122

1123
        return null;
×
1124
    }
1125

1126
    /**
1127
     * Returns an array of children on the post as Timber\Posts
1128
     * (or other claass as you define).
1129
     *
1130
     * @api
1131
     * @example
1132
     * ```twig
1133
     * {% if post.children is not empty %}
1134
     *     Here are the child pages:
1135
     *     {% for child in post.children %}
1136
     *         <a href="{{ child.link }}">{{ child.title }}</a>
1137
     *     {% endfor %}
1138
     * {% endif %}
1139
     * ```
1140
     * @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).
1141
     * @return PostCollectionInterface
1142
     */
1143
    public function children($args = 'any')
5✔
1144
    {
1145
        if (\is_string($args) || \array_values($args) === $args) {
5✔
1146
            $args = [
4✔
1147
                'post_type' => 'parent' === $args ? $this->post_type : $args,
4✔
1148
            ];
4✔
1149
        }
1150

1151
        $args = \wp_parse_args($args, [
5✔
1152
            'post_parent' => $this->ID,
5✔
1153
            'post_type' => 'any',
5✔
1154
            'posts_per_page' => -1,
5✔
1155
            'orderby' => 'menu_order title',
5✔
1156
            'order' => 'ASC',
5✔
1157
            'post_status' => 'publish' === $this->post_status ? ['publish', 'inherit'] : 'publish',
5✔
1158
        ]);
5✔
1159

1160
        /**
1161
         * Filters the arguments for the query used to get the children of a post.
1162
         *
1163
         * This filter is used by the `Timber\Post::children()` method. It allows you to modify the
1164
         * arguments for the `get_children` function. This way you can change the query to get the
1165
         * children of a post.
1166
         *
1167
         * @example
1168
         * ```
1169
         * add_filter( 'timber/post/children_args', function( $args, $post ) {
1170
         *
1171
         *     if ( $post->post_type === 'custom_post_type' ) {
1172
         *        $args['post_status'] = 'private';
1173
         *     }
1174
         *
1175
         *     return $args;
1176
         * } );
1177
         * ```
1178
         *
1179
         * @see   \Timber\Post::children()
1180
         * @since 2.1.0
1181
         *
1182
         * @param array        $arguments An array of arguments for the `get_children` function.
1183
         * @param Post $post   The post object.
1184
         */
1185
        $args = \apply_filters('timber/post/children_args', $args, $this);
5✔
1186

1187
        return $this->factory()->from(\get_children($args));
5✔
1188
    }
1189

1190
    /**
1191
     * Gets the comments on a Timber\Post and returns them as an array of `Timber\Comment` objects (or whatever comment class you set).
1192
     *
1193
     * @api
1194
     * Gets the comments on a `Timber\Post` and returns them as a `Timber\CommentThread`: a PHP
1195
     * ArrayObject of [`Timber\Comment`](https://timber.github.io/docs/v2/reference/timber-comment/)
1196
     * (or whatever comment class you set).
1197
     * @api
1198
     *
1199
     * @param int    $count        Set the number of comments you want to get. `0` is analogous to
1200
     *                             "all".
1201
     * @param string $order        Use ordering set in WordPress admin, or a different scheme.
1202
     * @param string $type         For when other plugins use the comments table for their own
1203
     *                             special purposes. Might be set to 'liveblog' or other, depending
1204
     *                             on what’s stored in your comments table.
1205
     * @param string $status       Could be 'pending', etc.
1206
     * @see CommentThread for an example with nested comments
1207
     * @return bool|CommentThread
1208
     *
1209
     * @example
1210
     *
1211
     * **single.twig**
1212
     *
1213
     * ```twig
1214
     * <div id="post-comments">
1215
     *   <h4>Comments on {{ post.title }}</h4>
1216
     *   <ul>
1217
     *     {% for comment in post.comments() %}
1218
     *       {% include 'comment.twig' %}
1219
     *     {% endfor %}
1220
     *   </ul>
1221
     *   <div class="comment-form">
1222
     *     {{ function('comment_form') }}
1223
     *   </div>
1224
     * </div>
1225
     * ```
1226
     *
1227
     * **comment.twig**
1228
     *
1229
     * ```twig
1230
     * {# comment.twig #}
1231
     * <li>
1232
     *   <p class="comment-author">{{ comment.author.name }} says:</p>
1233
     *   <div>{{ comment.content }}</div>
1234
     * </li>
1235
     * ```
1236
     */
1237
    public function comments($count = null, $order = 'wp', $type = 'comment', $status = 'approve')
13✔
1238
    {
1239
        global $overridden_cpage, $user_ID;
1240
        $overridden_cpage = false;
13✔
1241

1242
        $commenter = \wp_get_current_commenter();
13✔
1243
        $comment_author_email = $commenter['comment_author_email'];
13✔
1244

1245
        $args = [
13✔
1246
            'status' => $status,
13✔
1247
            'order' => $order,
13✔
1248
            'type' => $type,
13✔
1249
        ];
13✔
1250
        if ($count > 0) {
13✔
1251
            $args['number'] = $count;
1✔
1252
        }
1253
        if (\strtolower($order) == 'wp' || \strtolower($order) == 'wordpress') {
13✔
1254
            $args['order'] = \get_option('comment_order');
13✔
1255
        }
1256
        if ($user_ID) {
13✔
1257
            $args['include_unapproved'] = [$user_ID];
1✔
1258
        } elseif (!empty($comment_author_email)) {
13✔
1259
            $args['include_unapproved'] = [$comment_author_email];
1✔
1260
        } elseif (\function_exists('wp_get_unapproved_comment_author_email')) {
12✔
1261
            $unapproved_email = \wp_get_unapproved_comment_author_email();
12✔
1262
            if ($unapproved_email) {
12✔
1263
                $args['include_unapproved'] = [$unapproved_email];
1✔
1264
            }
1265
        }
1266
        $ct = new CommentThread($this->ID, false);
13✔
1267
        $ct->init($args);
13✔
1268
        return $ct;
13✔
1269
    }
1270

1271
    /**
1272
     * If the Password form is to be shown, show it!
1273
     * @return string|void
1274
     */
1275
    protected function maybe_show_password_form()
40✔
1276
    {
1277
        if ($this->password_required()) {
40✔
1278
            $show_pw = false;
3✔
1279

1280
            /**
1281
             * Filters whether the password form should be shown for password protected posts.
1282
             *
1283
             * This filter runs only when you call `{{ post.content }}` for a password protected
1284
             * post. When this filter returns `true`, a password form will be shown instead of the
1285
             * post content. If you want to modify the form itself, you can use the
1286
             * `timber/post/content/password_form` filter.
1287
             *
1288
             * @since 1.1.4
1289
             * @example
1290
             * ```php
1291
             * // Always show password form for password protected posts.
1292
             * add_filter( 'timber/post/content/show_password_form_for_protected', '__return_true' );
1293
             * ```
1294
             *
1295
             * @param bool $show_pw Whether the password form should be shown. Default `false`.
1296
             */
1297
            $show_pw = \apply_filters('timber/post/content/show_password_form_for_protected', $show_pw);
3✔
1298

1299
            if ($show_pw) {
3✔
1300
                /**
1301
                 * Filters the password form output.
1302
                 *
1303
                 * As an alternative to this filter, you could also use WordPress’s `the_password_form` filter.
1304
                 * The difference to this filter is, that you’ll also have the post object available as a second
1305
                 * parameter, in case you need that.
1306
                 *
1307
                 * @since 1.1.4
1308
                 *
1309
                 * @example
1310
                 * ```php
1311
                 * // Modify the password form.
1312
                 * add_filter( 'timber/post/content/password_form', function( $form, $post ) {
1313
                 *     return Timber::compile( 'assets/password-form.twig', array( 'post' => $post ) );
1314
                 * }, 10, 2 );
1315
                 * ```
1316
                 *
1317
                 * @param string       $form Form output. Default WordPress password form output generated by `get_the_password_form()`.
1318
                 * @param Post $post The post object.
1319
                 */
1320
                return \apply_filters('timber/post/content/password_form', \get_the_password_form($this->ID), $this);
2✔
1321
            }
1322
        }
1323
    }
1324

1325
    /**
1326
     *
1327
     */
1328
    protected function get_revised_data_from_method($method, $args = false)
149✔
1329
    {
1330
        if (!\is_array($args)) {
149✔
1331
            $args = [$args];
110✔
1332
        }
1333
        $rev = $this->get_post_preview_object();
149✔
1334
        if ($rev && $this->ID == $rev->post_parent && $this->ID != $rev->ID) {
149✔
1335
            return \call_user_func_array([$rev, $method], $args);
8✔
1336
        }
1337
    }
1338

1339
    /**
1340
     * Gets the actual content of a WordPress post.
1341
     *
1342
     * As opposed to using `{{ post.post_content }}`, this will run the hooks/filters attached to
1343
     * the `the_content` filter. It will return your post’s content with WordPress filters run on it
1344
     * – which means it will parse blocks, convert shortcodes or run `wpautop()` on the content.
1345
     *
1346
     * If you use page breaks in your content to split your post content into multiple pages,
1347
     * use `{{ post.paged_content }}` to display only the content for the current page.
1348
     *
1349
     * @api
1350
     * @example
1351
     * ```twig
1352
     * <article>
1353
     *     <h1>{{ post.title }}</h1>
1354
     *
1355
     *     <div class="content">{{ post.content }}</div>
1356
     * </article>
1357
     * ```
1358
     *
1359
     * @param int $page Optional. The page to show if the content of the post is split into multiple
1360
     *                  pages. Read more about this in the [Pagination Guide](https://timber.github.io/docs/v2/guides/pagination/#paged-content-within-a-post). Default `0`.
1361
     * @param int $len Optional. The number of words to show. Default `-1` (show all).
1362
     * @param bool $remove_blocks Optional. Whether to remove blocks. Defaults to false. True when called from the $post->excerpt() method.
1363
     * @return string The content of the post.
1364
     */
1365
    public function content($page = 0, $len = -1, $remove_blocks = false)
40✔
1366
    {
1367
        if ($rd = $this->get_revised_data_from_method('content', [$page, $len])) {
40✔
1368
            return $rd;
4✔
1369
        }
1370
        if ($form = $this->maybe_show_password_form()) {
40✔
1371
            return $form;
2✔
1372
        }
1373
        if ($len == -1 && $page == 0 && $this->___content) {
38✔
UNCOV
1374
            return $this->___content;
×
1375
        }
1376

1377
        $content = $this->post_content;
38✔
1378

1379
        if ($len > 0) {
38✔
1380
            $content = \wp_trim_words($content, $len);
1✔
1381
        }
1382

1383
        /**
1384
         * Page content split by <!--nextpage-->.
1385
         *
1386
         * @see WP_Query::generate_postdata()
1387
         */
1388
        if ($page && \str_contains((string) $content, '<!--nextpage-->')) {
38✔
1389
            $content = \str_replace("\n<!--nextpage-->\n", '<!--nextpage-->', (string) $content);
6✔
1390
            $content = \str_replace("\n<!--nextpage-->", '<!--nextpage-->', $content);
6✔
1391
            $content = \str_replace("<!--nextpage-->\n", '<!--nextpage-->', $content);
6✔
1392

1393
            // Remove the nextpage block delimiters, to avoid invalid block structures in the split content.
1394
            $content = \str_replace('<!-- wp:nextpage -->', '', $content);
6✔
1395
            $content = \str_replace('<!-- /wp:nextpage -->', '', $content);
6✔
1396

1397
            // Ignore nextpage at the beginning of the content.
1398
            if (\str_starts_with($content, '<!--nextpage-->')) {
6✔
1399
                $content = \substr($content, 15);
1✔
1400
            }
1401

1402
            $pages = \explode('<!--nextpage-->', $content);
6✔
1403
            $page--;
6✔
1404

1405
            if (\count($pages) > $page) {
6✔
1406
                $content = $pages[$page];
6✔
1407
            }
1408
        }
1409

1410
        /**
1411
         * Filters whether the content produced by block editor blocks should be removed or not from the content.
1412
         *
1413
         * If truthy then block whose content does not belong in the excerpt, will be removed.
1414
         * This removal is done using WordPress Core `excerpt_remove_blocks` function.
1415
         *
1416
         * @since 2.1.1
1417
         *
1418
         * @param bool $remove_blocks Whether blocks whose content should not be part of the excerpt should be removed
1419
         *                            or not from the excerpt.
1420
         *
1421
         * @see   excerpt_remove_blocks() The WordPress Core function that will handle the block removal from the excerpt.
1422
         */
1423
        $remove_blocks = (bool) \apply_filters('timber/post/content/remove_blocks', $remove_blocks);
38✔
1424

1425
        if ($remove_blocks) {
38✔
1426
            $content = \excerpt_remove_blocks($content);
18✔
1427
        }
1428

1429
        $content = $this->content_handle_no_teaser_block($content);
38✔
1430
        $content = \apply_filters('the_content', ($content));
38✔
1431

1432
        if ($len == -1 && $page == 0 && !$remove_blocks) {
38✔
1433
            $this->___content = $content;
20✔
1434
        }
1435

1436
        return $content;
38✔
1437
    }
1438

1439
    /**
1440
     * Handles for an circumstance with the Block editor where a "more" block has an option to
1441
     * "Hide the excerpt on the full content page" which hides everything prior to the inserted
1442
     * "more" block
1443
     * @ticket #2218
1444
     * @param string $content
1445
     * @return string
1446
     */
1447
    protected function content_handle_no_teaser_block($content)
38✔
1448
    {
1449
        if ((\str_contains($content, 'noTeaser:true') || \str_contains($content, '"noTeaser":true')) && \str_contains($content, '<!-- /wp:more -->')) {
38✔
1450
            $arr = \explode('<!-- /wp:more -->', $content);
1✔
1451
            return \trim($arr[1]);
1✔
1452
        }
1453
        return $content;
37✔
1454
    }
1455

1456
    /**
1457
     * Gets the paged content for a post.
1458
     *
1459
     * You will use this, if you use `<!--nextpage-->` in your post content or the Page Break block
1460
     * in the Block Editor. Use `{{ post.pagination }}` to create a pagination for your paged
1461
     * content. Learn more about this in the [Pagination Guide](https://timber.github.io/docs/v2/guides/pagination/#paged-content-within-a-post).
1462
     *
1463
     * @example
1464
     * ```twig
1465
     * {{ post.paged_content }}
1466
     * ```
1467
     *
1468
     * @return string The content for the current page. If there’s no page break found in the
1469
     *                content, the whole content is returned.
1470
     */
1471
    public function paged_content()
4✔
1472
    {
1473
        global $page;
1474
        return $this->content($page, -1);
4✔
1475
    }
1476

1477
    /**
1478
     * Gets the timestamp when the post was published.
1479
     *
1480
     * @api
1481
     * @since 2.0.0
1482
     *
1483
     * @return false|int Unix timestamp on success, false on failure.
1484
     */
1485
    public function timestamp()
10✔
1486
    {
1487
        return \get_post_timestamp($this->ID);
10✔
1488
    }
1489

1490
    /**
1491
     * Gets the timestamp when the post was last modified.
1492
     *
1493
     * @api
1494
     * @since 2.0.0
1495
     *
1496
     * @return false|int Unix timestamp on success, false on failure.
1497
     */
1498
    public function modified_timestamp()
4✔
1499
    {
1500
        return \get_post_timestamp($this->ID, 'modified');
4✔
1501
    }
1502

1503
    /**
1504
     * Gets the publishing date of the post.
1505
     *
1506
     * This function will also apply the
1507
     * [`get_the_date`](https://developer.wordpress.org/reference/hooks/get_the_date/) filter to the
1508
     * output.
1509
     *
1510
     * If you use {{ post.date }} with the |time_ago filter, then make sure that you use a time
1511
     * format including the full time and not just the date.
1512
     *
1513
     * @api
1514
     * @example
1515
     * ```twig
1516
     * {# Uses date format set in Settings → General #}
1517
     * Published on {{ post.date }}
1518
     * OR
1519
     * Published on {{ post.date('F jS') }}
1520
     * which was
1521
     * {{ post.date('U')|time_ago }}
1522
     * {{ post.date('Y-m-d H:i:s')|time_ago }}
1523
     * {{ post.date(constant('DATE_ATOM'))|time_ago }}
1524
     * ```
1525
     *
1526
     * ```html
1527
     * Published on January 12, 2015
1528
     * OR
1529
     * Published on Jan 12th
1530
     * which was
1531
     * 8 years ago
1532
     * ```
1533
     *
1534
     * @param string|null $date_format Optional. PHP date format. Will use the `date_format` option
1535
     *                                 as a default.
1536
     *
1537
     * @return string
1538
     */
1539
    public function date($date_format = null)
8✔
1540
    {
1541
        $format = $date_format ?: \get_option('date_format');
8✔
1542
        $date = \wp_date($format, $this->timestamp());
8✔
1543

1544
        /**
1545
         * Filters the date a post was published.
1546
         *
1547
         * @see get_the_date()
1548
         *
1549
         * @param string      $date        The formatted date.
1550
         * @param string      $date_format PHP date format. Defaults to 'date_format' option if not
1551
         *                                 specified.
1552
         * @param int|WP_Post $id          The post object or ID.
1553
         */
1554
        $date = \apply_filters('get_the_date', $date, $date_format, $this->ID);
8✔
1555

1556
        return $date;
8✔
1557
    }
1558

1559
    /**
1560
     * Gets the date the post was last modified.
1561
     *
1562
     * This function will also apply the
1563
     * [`get_the_modified_date`](https://developer.wordpress.org/reference/hooks/get_the_modified_date/)
1564
     * filter to the output.
1565
     *
1566
     * @api
1567
     * @example
1568
     * ```twig
1569
     * {# Uses date format set in Settings → General #}
1570
     * Last modified on {{ post.modified_date }}
1571
     * OR
1572
     * Last modified on {{ post.modified_date('F jS') }}
1573
     * ```
1574
     *
1575
     * ```html
1576
     * Last modified on January 12, 2015
1577
     * OR
1578
     * Last modified on Jan 12th
1579
     * ```
1580
     *
1581
     * @param string|null $date_format Optional. PHP date format. Will use the `date_format` option
1582
     *                                 as a default.
1583
     *
1584
     * @return string
1585
     */
1586
    public function modified_date($date_format = null)
2✔
1587
    {
1588
        $format = $date_format ?: \get_option('date_format');
2✔
1589
        $date = \wp_date($format, $this->modified_timestamp());
2✔
1590

1591
        /**
1592
         * Filters the date a post was last modified.
1593
         *
1594
         * This filter expects a `WP_Post` object as the last parameter. We only have a
1595
         * `Timber\Post` object available, that wouldn’t match the expected argument. That’s why we
1596
         * need to get the post object with get_post(). This is fairly inexpensive, because the post
1597
         * will already be in the cache.
1598
         *
1599
         * @see get_the_modified_date()
1600
         *
1601
         * @param string|bool  $date        The formatted date or false if no post is found.
1602
         * @param string       $date_format PHP date format. Defaults to value specified in
1603
         *                                  'date_format' option.
1604
         * @param WP_Post|null $post        WP_Post object or null if no post is found.
1605
         */
1606
        $date = \apply_filters('get_the_modified_date', $date, $date_format, \get_post($this->ID));
2✔
1607

1608
        return $date;
2✔
1609
    }
1610

1611
    /**
1612
     * Gets the time the post was published to use in your template.
1613
     *
1614
     * This function will also apply the
1615
     * [`get_the_time`](https://developer.wordpress.org/reference/hooks/get_the_time/) filter to the
1616
     * output.
1617
     *
1618
     * @api
1619
     * @example
1620
     * ```twig
1621
     * {# Uses time format set in Settings → General #}
1622
     * Published at {{ post.time }}
1623
     * OR
1624
     * Published at {{ post.time('G:i') }}
1625
     * ```
1626
     *
1627
     * ```html
1628
     * Published at 1:25 pm
1629
     * OR
1630
     * Published at 13:25
1631
     * ```
1632
     *
1633
     * @param string|null $time_format Optional. PHP date format. Will use the `time_format` option
1634
     *                                 as a default.
1635
     *
1636
     * @return string
1637
     */
1638
    public function time($time_format = null)
2✔
1639
    {
1640
        $format = $time_format ?: \get_option('time_format');
2✔
1641
        $time = \wp_date($format, $this->timestamp());
2✔
1642

1643
        /**
1644
         * Filters the time a post was written.
1645
         *
1646
         * @see get_the_time()
1647
         *
1648
         * @param string      $time        The formatted time.
1649
         * @param string      $time_format Format to use for retrieving the time the post was
1650
         *                                 written. Accepts 'G', 'U', or php date format value
1651
         *                                 specified in `time_format` option. Default empty.
1652
         * @param int|WP_Post $id          WP_Post object or ID.
1653
         */
1654
        $time = \apply_filters('get_the_time', $time, $time_format, $this->ID);
2✔
1655

1656
        return $time;
2✔
1657
    }
1658

1659
    /**
1660
     * Gets the time of the last modification of the post to use in your template.
1661
     *
1662
     * This function will also apply the
1663
     * [`get_the_time`](https://developer.wordpress.org/reference/hooks/get_the_modified_time/)
1664
     * filter to the output.
1665
     *
1666
     * @api
1667
     * @example
1668
     * ```twig
1669
     * {# Uses time format set in Settings → General #}
1670
     * Published at {{ post.modified_time }}
1671
     * OR
1672
     * Published at {{ post.modified_time('G:i') }}
1673
     * ```
1674
     *
1675
     * ```html
1676
     * Published at 1:25 pm
1677
     * OR
1678
     * Published at 13:25
1679
     * ```
1680
     *
1681
     * @param string|null $time_format Optional. PHP date format. Will use the `time_format` option
1682
     *                                 as a default.
1683
     *
1684
     * @return string
1685
     */
1686
    public function modified_time($time_format = null)
2✔
1687
    {
1688
        $format = $time_format ?: \get_option('time_format');
2✔
1689
        $time = \wp_date($format, $this->modified_timestamp());
2✔
1690

1691
        /**
1692
         * Filters the localized time a post was last modified.
1693
         *
1694
         * This filter expects a `WP_Post` object as the last parameter. We only have a
1695
         * `Timber\Post` object available, that wouldn’t match the expected argument. That’s why we
1696
         * need to get the post object with get_post(). This is fairly inexpensive, because the post
1697
         * will already be in the cache.
1698
         *
1699
         * @see get_the_modified_time()
1700
         *
1701
         * @param string|bool  $time        The formatted time or false if no post is found.
1702
         * @param string       $time_format Format to use for retrieving the time the post was
1703
         *                                  written. Accepts 'G', 'U', or php date format. Defaults
1704
         *                                  to value specified in 'time_format' option.
1705
         * @param WP_Post|null $post        WP_Post object or null if no post is found.
1706
         */
1707
        $time = \apply_filters('get_the_modified_time', $time, $time_format, \get_post($this->ID));
2✔
1708

1709
        return $time;
2✔
1710
    }
1711

1712
    /**
1713
     * Returns the PostType object for a post’s post type with labels and other info.
1714
     *
1715
     * @api
1716
     * @since 1.0.4
1717
     * @example
1718
     * ```twig
1719
     * This post is from <span>{{ post.type.labels.name }}</span>
1720
     * ```
1721
     *
1722
     * ```html
1723
     * This post is from <span>Recipes</span>
1724
     * ```
1725
     * @return PostType
1726
     */
1727
    public function type()
3✔
1728
    {
1729
        if (!$this->__type instanceof PostType) {
3✔
1730
            $this->__type = new PostType($this->post_type);
3✔
1731
        }
1732
        return $this->__type;
3✔
1733
    }
1734

1735
    /**
1736
     * Checks whether the current user can edit the post.
1737
     *
1738
     * @api
1739
     * @example
1740
     * ```twig
1741
     * {% if post.can_edit %}
1742
     *     <a href="{{ post.edit_link }}">Edit</a>
1743
     * {% endif %}
1744
     * ```
1745
     * @return bool
1746
     */
1747
    public function can_edit(): bool
2✔
1748
    {
1749
        return \current_user_can('edit_post', $this->ID);
2✔
1750
    }
1751

1752
    /**
1753
     * Gets the edit link for a post if the current user has the correct rights.
1754
     *
1755
     * @api
1756
     * @example
1757
     * ```twig
1758
     * {% if post.can_edit %}
1759
     *     <a href="{{ post.edit_link }}">Edit</a>
1760
     * {% endif %}
1761
     * ```
1762
     * @return string|null The edit URL of a post in the WordPress admin or null if the current user can’t edit the
1763
     *                     post.
1764
     */
1765
    public function edit_link(): ?string
1✔
1766
    {
1767
        if (!$this->can_edit()) {
1✔
1768
            return null;
1✔
1769
        }
1770

1771
        return \get_edit_post_link($this->ID);
1✔
1772
    }
1773

1774
    /**
1775
     * @api
1776
     * @return mixed
1777
     */
1778
    public function format()
1✔
1779
    {
1780
        return \get_post_format($this->ID);
1✔
1781
    }
1782

1783
    /**
1784
     * whether post requires password and correct password has been provided
1785
     * @api
1786
     * @return boolean
1787
     */
1788
    public function password_required()
41✔
1789
    {
1790
        return \post_password_required($this->ID);
41✔
1791
    }
1792

1793
    /**
1794
     * get the permalink for a post object
1795
     * @api
1796
     * @example
1797
     * ```twig
1798
     * <a href="{{post.link}}">Read my post</a>
1799
     * ```
1800
     * @return string ex: https://example.org/2015/07/my-awesome-post
1801
     */
1802
    public function link()
33✔
1803
    {
1804
        if (isset($this->_permalink)) {
33✔
1805
            return $this->_permalink;
14✔
1806
        }
1807
        $this->_permalink = \get_permalink($this->ID);
33✔
1808
        return $this->_permalink;
33✔
1809
    }
1810

1811
    /**
1812
     * @api
1813
     * @return string
1814
     */
1815
    public function name()
1✔
1816
    {
1817
        return $this->title();
1✔
1818
    }
1819

1820
    /**
1821
     * Gets the next post that is adjacent to the current post in a collection.
1822
     *
1823
     * Works pretty much the same as
1824
     * [`get_next_post()`](https://developer.wordpress.org/reference/functions/get_next_post/).
1825
     *
1826
     * @api
1827
     * @example
1828
     * ```twig
1829
     * {% if post.next %}
1830
     *     <a href="{{ post.next.link }}">{{ post.next.title }}</a>
1831
     * {% endif %}
1832
     * ```
1833
     * @param bool|string $in_same_term Whether the post should be in a same taxonomy term. Default
1834
     *                                  `false`.
1835
     *
1836
     * @return mixed
1837
     */
1838
    public function next($in_same_term = false)
5✔
1839
    {
1840
        if (!isset($this->_next) || !isset($this->_next[$in_same_term])) {
5✔
1841
            global $post;
1842
            $this->_next = [];
5✔
1843
            $old_global = $post;
5✔
1844
            $post = $this;
5✔
1845
            if (\is_string($in_same_term) && \strlen($in_same_term)) {
5✔
1846
                $adjacent = \get_adjacent_post(true, '', false, $in_same_term);
2✔
1847
            } else {
1848
                $adjacent = \get_adjacent_post(false, '', false);
3✔
1849
            }
1850

1851
            if ($adjacent) {
5✔
1852
                $this->_next[$in_same_term] = $this->factory()->from($adjacent);
4✔
1853
            } else {
1854
                $this->_next[$in_same_term] = false;
1✔
1855
            }
1856
            $post = $old_global;
5✔
1857
        }
1858
        return $this->_next[$in_same_term];
5✔
1859
    }
1860

1861
    /**
1862
     * Gets a data array to display a pagination for your paginated post.
1863
     *
1864
     * Use this in combination with `{{ post.paged_content }}`.
1865
     *
1866
     * @api
1867
     * @example
1868
     * Using simple links to the next an previous page.
1869
     * ```twig
1870
     * {% if post.pagination.next is not empty %}
1871
     *     <a href="{{ post.pagination.next.link|esc_url }}">Go to next page</a>
1872
     * {% endif %}
1873
     *
1874
     * {% if post.pagination.prev is not empty %}
1875
     *     <a href="{{ post.pagination.prev.link|esc_url }}">Go to previous page</a>
1876
     * {% endif %}
1877
     * ```
1878
     * Using a pagination for all pages.
1879
     * ```twig
1880
     * {% if post.pagination.pages is not empty %}
1881
     *    <nav aria-label="pagination">
1882
     *        <ul>
1883
     *            {% for page in post.pagination.pages %}
1884
     *                <li>
1885
     *                    {% if page.current %}
1886
     *                        <span aria-current="page">Page {{ page.title }}</span>
1887
     *                    {% else %}
1888
     *                        <a href="{{ page.link|esc_ur }}">Page {{ page.title }}</a>
1889
     *                    {% endif %}
1890
     *                </li>
1891
     *            {% endfor %}
1892
     *        </ul>
1893
     *    </nav>
1894
     * {% endif %}
1895
     * ```
1896
     *
1897
     * @return array An array with data to build your paginated content.
1898
     */
1899
    public function pagination()
4✔
1900
    {
1901
        global $post, $page, $numpages, $multipage;
1902
        $post = $this;
4✔
1903
        $ret = [];
4✔
1904
        if ($multipage) {
4✔
1905
            for ($i = 1; $i <= $numpages; $i++) {
4✔
1906
                $link = self::get_wp_link_page($i);
4✔
1907
                $data = [
4✔
1908
                    'name' => $i,
4✔
1909
                    'title' => $i,
4✔
1910
                    'text' => $i,
4✔
1911
                    'link' => $link,
4✔
1912
                ];
4✔
1913
                if ($i == $page) {
4✔
1914
                    $data['current'] = true;
4✔
1915
                }
1916
                $ret['pages'][] = $data;
4✔
1917
            }
1918
            $i = $page - 1;
4✔
1919
            if ($i) {
4✔
1920
                $link = self::get_wp_link_page($i);
×
1921
                $ret['prev'] = [
×
1922
                    'link' => $link,
×
1923
                ];
×
1924
            }
1925
            $i = $page + 1;
4✔
1926
            if ($i <= $numpages) {
4✔
1927
                $link = self::get_wp_link_page($i);
4✔
1928
                $ret['next'] = [
4✔
1929
                    'link' => $link,
4✔
1930
                ];
4✔
1931
            }
1932
        }
1933
        return $ret;
4✔
1934
    }
1935

1936
    /**
1937
     * Finds any WP_Post objects and converts them to Timber\Post objects.
1938
     *
1939
     * @api
1940
     * @param array|WP_Post $data
1941
     */
1942
    public function convert($data)
17✔
1943
    {
1944
        if (\is_object($data)) {
17✔
1945
            $data = Helper::convert_wp_object($data);
15✔
1946
        } elseif (\is_array($data)) {
7✔
1947
            $data = \array_map([$this, 'convert'], $data);
6✔
1948
        }
1949
        return $data;
17✔
1950
    }
1951

1952
    /**
1953
     * Gets the parent (if one exists) from a post as a Timber\Post object.
1954
     * Honors Class Maps.
1955
     *
1956
     * @api
1957
     * @example
1958
     * ```twig
1959
     * Parent page: <a href="{{ post.parent.link }}">{{ post.parent.title }}</a>
1960
     * ```
1961
     * @return bool|Post
1962
     */
1963
    public function parent()
3✔
1964
    {
1965
        if (!$this->post_parent) {
3✔
1966
            return false;
1✔
1967
        }
1968

1969
        return $this->factory()->from($this->post_parent);
2✔
1970
    }
1971

1972
    /**
1973
     * Gets the relative path of a WP Post, so while link() will return https://example.org/2015/07/my-cool-post
1974
     * this will return just /2015/07/my-cool-post
1975
     *
1976
     * @api
1977
     * @example
1978
     * ```twig
1979
     * <a href="{{post.path}}">{{post.title}}</a>
1980
     * ```
1981
     * @return string
1982
     */
1983
    public function path()
3✔
1984
    {
1985
        return URLHelper::get_rel_url($this->link());
3✔
1986
    }
1987

1988
    /**
1989
     * Get the previous post that is adjacent to the current post in a collection.
1990
     *
1991
     * Works pretty much the same as
1992
     * [`get_previous_post()`](https://developer.wordpress.org/reference/functions/get_previous_post/).
1993
     *
1994
     * @api
1995
     * @example
1996
     * ```twig
1997
     * {% if post.prev %}
1998
     *     <a href="{{ post.prev.link }}">{{ post.prev.title }}</a>
1999
     * {% endif %}
2000
     * ```
2001
     * @param bool|string $in_same_term Whether the post should be in a same taxonomy term. Default
2002
     *                                  `false`.
2003
     * @return mixed
2004
     */
2005
    public function prev($in_same_term = false)
3✔
2006
    {
2007
        if (isset($this->_prev) && isset($this->_prev[$in_same_term])) {
3✔
2008
            return $this->_prev[$in_same_term];
×
2009
        }
2010
        global $post;
2011
        $old_global = $post;
3✔
2012
        $post = $this;
3✔
2013
        $within_taxonomy = $in_same_term ?: 'category';
3✔
2014
        $adjacent = \get_adjacent_post(($in_same_term), '', true, $within_taxonomy);
3✔
2015
        $prev_in_taxonomy = false;
3✔
2016
        if ($adjacent) {
3✔
2017
            $prev_in_taxonomy = $this->factory()->from($adjacent);
3✔
2018
        }
2019
        $this->_prev[$in_same_term] = $prev_in_taxonomy;
3✔
2020
        $post = $old_global;
3✔
2021
        return $this->_prev[$in_same_term];
3✔
2022
    }
2023

2024
    /**
2025
     * Gets the tags on a post, uses WP's post_tag taxonomy
2026
     *
2027
     * @api
2028
     * @return array
2029
     */
2030
    public function tags()
3✔
2031
    {
2032
        return $this->terms('post_tag');
3✔
2033
    }
2034

2035
    /**
2036
     * Gets the post’s thumbnail ID.
2037
     *
2038
     * @api
2039
     * @since 2.0.0
2040
     *
2041
     * @return false|int The default post’s ID. False if no thumbnail was defined.
2042
     */
2043
    public function thumbnail_id()
25✔
2044
    {
2045
        return (int) \get_post_meta($this->ID, '_thumbnail_id', true);
25✔
2046
    }
2047

2048
    /**
2049
     * get the featured image as a Timber/Image
2050
     *
2051
     * @api
2052
     * @example
2053
     * ```twig
2054
     * <img src="{{ post.thumbnail.src }}" />
2055
     * ```
2056
     * @return Image|null of your thumbnail
2057
     */
2058
    public function thumbnail()
24✔
2059
    {
2060
        $tid = $this->thumbnail_id();
24✔
2061

2062
        if ($tid) {
24✔
2063
            return $this->factory()->from($tid);
22✔
2064
        }
2065

2066
        return null;
2✔
2067
    }
2068

2069
    /**
2070
     * 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.
2071
     *
2072
     * @api
2073
     * @example
2074
     * ```twig
2075
     * <h1>{{ post.title }}</h1>
2076
     * ```
2077
     * @return string
2078
     */
2079
    public function title()
54✔
2080
    {
2081
        if ($rd = $this->get_revised_data_from_method('title')) {
54✔
2082
            return $rd;
1✔
2083
        }
2084
        return \apply_filters('the_title', $this->post_title, $this->ID);
54✔
2085
    }
2086

2087
    /**
2088
     * Returns galleries from the post’s content.
2089
     *
2090
     * @api
2091
     * @example
2092
     * ```twig
2093
     * {{ post.gallery }}
2094
     * ```
2095
     * @return array A list of arrays, each containing gallery data and srcs parsed from the
2096
     * expanded shortcode.
2097
     */
2098
    public function gallery($html = true)
1✔
2099
    {
2100
        $galleries = \get_post_galleries($this->ID, $html);
1✔
2101
        $gallery = \reset($galleries);
1✔
2102

2103
        return \apply_filters('get_post_gallery', $gallery, $this->ID, $galleries);
1✔
2104
    }
2105

2106
    protected function get_entity_name()
×
2107
    {
2108
        return 'post';
×
2109
    }
2110

2111
    /**
2112
     * Given a base query and a list of taxonomies, return a list of queries
2113
     * each of which queries for one of the taxonomies.
2114
     * @example
2115
     * ```
2116
     * $this->partition_tax_queries(["object_ids" => [123]], ["a", "b"]);
2117
     *
2118
     * // result:
2119
     * // [
2120
     * //   ["object_ids" => [123], "taxonomy" => ["a"]],
2121
     * //   ["object_ids" => [123], "taxonomy" => ["b"]],
2122
     * // ]
2123
     * ```
2124
     * @internal
2125
     */
2126
    private function partition_tax_queries(array $query, array $taxonomies): array
3✔
2127
    {
2128
        return \array_map(fn (string $tax): array => \array_merge($query, [
3✔
2129
            'taxonomy' => [$tax],
3✔
2130
        ]), $taxonomies);
3✔
2131
    }
2132

2133
    /**
2134
     * Get a PostFactory instance for internal usage
2135
     *
2136
     * @internal
2137
     * @return PostFactory
2138
     */
2139
    private function factory()
43✔
2140
    {
2141
        static $factory;
43✔
2142
        $factory = $factory ?: new PostFactory();
43✔
2143
        return $factory;
43✔
2144
    }
2145
}
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