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

timber / timber / 21904947821

11 Feb 2026 12:24PM UTC coverage: 89.673% (+0.07%) from 89.608%
21904947821

Pull #3023

travis-ci

web-flow
Merge 70be7c33f into d6c4d6191
Pull Request #3023: Add render_twig_block method

65 of 67 new or added lines in 4 files covered. (97.01%)

38 existing lines in 6 files now uncovered.

4637 of 5171 relevant lines covered (89.67%)

65.94 hits per line

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

94.71
/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
436✔
185
    {
186
        $post = new static();
436✔
187

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

192
        $data = \get_object_vars($wp_post);
436✔
193
        $data = $post->get_info($data);
436✔
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);
436✔
206

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

209
        return $post;
436✔
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()
436✔
219
    {
220
    }
436✔
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
436✔
522
    {
523
        $data = \array_merge($data, [
436✔
524
            'slug' => $this->wp_object->post_name,
436✔
525
            'status' => $this->wp_object->post_status,
436✔
526
        ]);
436✔
527

528
        return $data;
436✔
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()
7✔
904
    {
905
        if (isset($this->_ancestors)) {
7✔
906
            return $this->_ancestors;
1✔
907
        }
908

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

911
        return $this->_ancestors = \is_iterable($ancestors) ? $ancestors : new PostArrayObject([]);
7✔
912
    }
913

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

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

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

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

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

UNCOV
1013
        return null;
×
1014
    }
1015

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1326
        return $content;
36✔
1327
    }
1328

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

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

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

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

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

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

1446
        return $date;
8✔
1447
    }
1448

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

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

1498
        return $date;
2✔
1499
    }
1500

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

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

1546
        return $time;
2✔
1547
    }
1548

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

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

1599
        return $time;
2✔
1600
    }
1601

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1956
        return null;
2✔
1957
    }
1958

1959
    /**
1960
     * 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.
1961
     *
1962
     * @api
1963
     * @example
1964
     * ```twig
1965
     * <h1>{{ post.title }}</h1>
1966
     * ```
1967
     * @return string
1968
     */
1969
    public function title()
54✔
1970
    {
1971
        if ($rd = $this->get_revised_data_from_method('title')) {
54✔
1972
            return $rd;
1✔
1973
        }
1974
        return \apply_filters('the_title', $this->post_title, $this->ID);
54✔
1975
    }
1976

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

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

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

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

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