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

timber / timber / 5690593717

pending completion
5690593717

Pull #1617

github

web-flow
Merge f587ceffa into b563d274e
Pull Request #1617: 2.x

4433 of 4433 new or added lines in 57 files covered. (100.0%)

3931 of 4433 relevant lines covered (88.68%)

58.28 hits per line

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

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

3
namespace Timber;
4

5
use SimpleXMLElement;
6

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
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;
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 string Stores the CSS classes for the post (ex: "post post-type-book post-123")
96
     */
97
    protected $_css_class;
98

99
    /**
100
     * @api
101
     * @var int The numeric WordPress id of a post.
102
     */
103
    public $id;
104

105
    /**
106
     * @api
107
     * @var int The numeric WordPress id of a post, capitalized to match WordPress usage.
108
     */
109
    public $ID;
110

111
    /**
112
     * @api
113
     * @var int The numeric ID of the a post's author corresponding to the wp_user database table
114
     */
115
    public $post_author;
116

117
    /**
118
     * @api
119
     * @var string The raw text of a WP post as stored in the database
120
     */
121
    public $post_content;
122

123
    /**
124
     * @api
125
     * @var string The raw date string as stored in the WP database, ex: 2014-07-05 18:01:39
126
     */
127
    public $post_date;
128

129
    /**
130
     * @api
131
     * @var string The raw text of a manual post excerpt as stored in the database
132
     */
133
    public $post_excerpt;
134

135
    /**
136
     * @api
137
     * @var int The numeric ID of a post's parent post
138
     */
139
    public $post_parent;
140

141
    /**
142
     * @api
143
     * @var string The status of a post ("draft", "publish", etc.)
144
     */
145
    public $post_status;
146

147
    /**
148
     * @api
149
     * @var string The raw text of a post's title as stored in the database
150
     */
151
    public $post_title;
152

153
    /**
154
     * @api
155
     * @var string The name of the post type, this is the machine name (so "my_custom_post_type" as
156
     *      opposed to "My Custom Post Type")
157
     */
158
    public $post_type;
159

160
    /**
161
     * @api
162
     * @var string The URL-safe slug, this corresponds to the poorly-named "post_name" in the WP
163
     *      database, ex: "hello-world"
164
     */
165
    public $slug;
166

167
    /**
168
     * @var string Stores the PostType object for the post.
169
     */
170
    protected $__type;
171

172
    /**
173
     * Create and initialize a new instance of the called Post class
174
     * (i.e. Timber\Post or a subclass).
175
     *
176
     * @internal
177
     * @return \Timber\Post
178
     */
179
    public static function build(WP_Post $wp_post): self
180
    {
181
        $post = new static();
420✔
182

183
        $post->id = $wp_post->ID;
420✔
184
        $post->ID = $wp_post->ID;
420✔
185
        $post->wp_object = $wp_post;
420✔
186

187
        $data = \get_object_vars($wp_post);
420✔
188
        $data = $post->get_info($data);
420✔
189

190
        /**
191
         * Filters the imported post data.
192
         *
193
         * Used internally for previews.
194
         *
195
         * @since 2.0.0
196
         * @see   Timber::init()
197
         * @param array        $data An array of post data to import.
198
         * @param \Timber\Post $post The Timber post instance.
199
         */
200
        $data = \apply_filters('timber/post/import_data', $data, $post);
420✔
201

202
        $post->import($data);
420✔
203

204
        return $post;
420✔
205
    }
206

207
    /**
208
     * If you send the constructor nothing it will try to figure out the current post id based on
209
     * being inside The_Loop.
210
     *
211
     * @internal
212
     */
213
    final protected function __construct()
214
    {
215
    }
420✔
216

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

233
        if ('_thumbnail_id' === $field) {
16✔
234
            Helper::doing_it_wrong(
2✔
235
                "Accessing the thumbnail ID through {{ {$this->object_type}._thumbnail_id }}",
2✔
236
                "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✔
237
                '2.0.0'
2✔
238
            );
2✔
239
        }
240

241
        return parent::__get($field);
16✔
242
    }
243

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

261
        return parent::__call($field, $args);
10✔
262
    }
263

264
    /**
265
     * Gets the underlying WordPress Core object.
266
     *
267
     * @since 2.0.0
268
     *
269
     * @return WP_Post|null
270
     */
271
    public function wp_object(): ?WP_Post
272
    {
273
        return $this->wp_object;
1✔
274
    }
275

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

295
        // Mimick WordPress behavior to improve compatibility with third party plugins.
296
        $wp_query->in_the_loop = true;
31✔
297

298
        if (!$this->wp_object) {
31✔
299
            return $this;
×
300
        }
301

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

313
        // The setup_postdata() function will call the 'the_post' action.
314
        $wp_query->setup_postdata($this->wp_object);
31✔
315

316
        return $this;
31✔
317
    }
318

319
    /**
320
     * Resets variables after post has been used.
321
     *
322
     * This function will be called automatically when you loop over Timber posts.
323
     *
324
     * @api
325
     * @since 2.0.0
326
     *
327
     * @return \Timber\Post The post instance.
328
     */
329
    public function teardown()
330
    {
331
        global $wp_query;
332

333
        $wp_query->in_the_loop = false;
20✔
334

335
        return $this;
20✔
336
    }
337

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

350
    /**
351
     * Outputs the title of the post if you do something like `<h1>{{post}}</h1>`
352
     *
353
     * @api
354
     * @return string
355
     */
356
    public function __toString()
357
    {
358
        return $this->title();
1✔
359
    }
360

361
    protected function get_post_preview_object()
362
    {
363
        global $wp_query;
364
        if ($this->is_previewing()) {
204✔
365
            $revision_id = $this->get_post_preview_id($wp_query);
9✔
366
            return Timber::get_post($revision_id);
9✔
367
        }
368
    }
369

370
    protected function get_post_preview_id($query)
371
    {
372
        $can = [
9✔
373
            \get_post_type_object($query->queried_object->post_type)->cap->edit_post,
9✔
374
        ];
9✔
375

376
        if ($query->queried_object->author_id !== \get_current_user_id()) {
9✔
377
            $can[] = \get_post_type_object($query->queried_object->post_type)->cap->edit_others_posts;
9✔
378
        }
379

380
        $can_preview = [];
9✔
381

382
        foreach ($can as $type) {
9✔
383
            if (\current_user_can($type, $query->queried_object_id)) {
9✔
384
                $can_preview[] = true;
8✔
385
            }
386
        }
387

388
        if (\count($can_preview) !== \count($can)) {
9✔
389
            return;
1✔
390
        }
391

392
        $revisions = \wp_get_post_revisions($query->queried_object_id);
8✔
393

394
        if (!empty($revisions)) {
8✔
395
            $revision = \reset($revisions);
8✔
396
            return $revision->ID;
8✔
397
        }
398

399
        return false;
×
400
    }
401

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

414
        if (isset($this->ID)) {
1✔
415
            \update_post_meta($this->ID, $field, $value);
1✔
416
            $this->$field = $value;
1✔
417
        }
418
    }
419

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

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

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

506
        return null;
×
507
    }
508

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

526
        return $data;
420✔
527
    }
528

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

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

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

628
        // Defaults.
629
        $query_args = \wp_parse_args($query_args, [
15✔
630
            'taxonomy' => 'all',
15✔
631
        ]);
15✔
632

633
        $options = \wp_parse_args($options, [
15✔
634
            'merge' => true,
15✔
635
        ]);
15✔
636

637
        $taxonomies = $query_args['taxonomy'];
15✔
638
        $merge = $options['merge'];
15✔
639

640
        if (\in_array($taxonomies, ['all', 'any', ''])) {
15✔
641
            $taxonomies = \get_object_taxonomies($this->post_type);
3✔
642
        }
643

644
        if (!\is_array($taxonomies)) {
15✔
645
            $taxonomies = [$taxonomies];
12✔
646
        }
647

648
        $query = \array_merge($query_args, [
15✔
649
            'object_ids' => [$this->ID],
15✔
650
            'taxonomy' => $taxonomies,
15✔
651
        ]);
15✔
652

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

658
            // zip 'em up with the right keys
659
            return \array_combine($taxonomies, $termGroups);
3✔
660
        }
661

662
        return Timber::get_terms($query, $options);
13✔
663
    }
664

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

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

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

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

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

746
        if ($revised_data) {
126✔
747
            return $revised_data;
3✔
748
        }
749

750
        return parent::fetch_meta($field_name, $args, $apply_filters);
126✔
751
    }
752

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

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

776
        return $this->meta($field_name);
1✔
777
    }
778

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

794
        $this->$field_name = $this->meta($field_name);
×
795
    }
796

797
    /**
798
     * Get the CSS classes for a post without cache.
799
     * For usage you should use `{{post.class}}`
800
     *
801
     * @internal
802
     * @param string $class additional classes you want to add.
803
     * @example
804
     * ```twig
805
     * <article class="{{ post.post_class }}">
806
     *    {# Some stuff here #}
807
     * </article>
808
     * ```
809
     *
810
     * ```html
811
     * <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">
812
     *    {# Some stuff here #}
813
     * </article>
814
     * ```
815
     * @return string a space-seperated list of classes
816
     */
817
    public function post_class($class = '')
818
    {
819
        global $post;
820
        $old_global_post = $post;
6✔
821
        $post = $this;
6✔
822

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

829
        $post = $old_global_post;
6✔
830
        return $class_array;
6✔
831
    }
832

833
    /**
834
     * 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}}`
835
     *
836
     * @internal
837
     * @param string $class additional classes you want to add.
838
     * @see \Timber\Post::$_css_class
839
     * @example
840
     * ```twig
841
     * <article class="{{ post.class }}">
842
     *    {# Some stuff here #}
843
     * </article>
844
     * ```
845
     *
846
     * @return string a space-seperated list of classes
847
     */
848
    public function css_class($class = '')
849
    {
850
        if (!$this->_css_class) {
5✔
851
            $this->_css_class = $this->post_class();
5✔
852
        }
853

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

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

884
    /**
885
     * Return the author of a post
886
     *
887
     * @api
888
     * @example
889
     * ```twig
890
     * <h1>{{post.title}}</h1>
891
     * <p class="byline">
892
     *     <a href="{{post.author.link}}">{{post.author.name}}</a>
893
     * </p>
894
     * ```
895
     * @return User|null A User object if found, false if not
896
     */
897
    public function author()
898
    {
899
        if (isset($this->post_author)) {
10✔
900
            $factory = new UserFactory();
10✔
901
            return $factory->from((int) $this->post_author);
10✔
902
        }
903
    }
904

905
    /**
906
     * Got more than one author? That's cool, but you'll need Co-Authors plus or another plugin to access any data
907
     *
908
     * @api
909
     * @return array
910
     */
911
    public function authors()
912
    {
913
        /**
914
         * Filters authors for a post.
915
         *
916
         * This filter is used by the CoAuthorsPlus integration.
917
         *
918
         * @todo  Add example
919
         *
920
         * @see   \Timber\Post::authors()
921
         * @since 1.1.4
922
         *
923
         * @param array        $authors An array of User objects. Default: User object for `post_author`.
924
         * @param \Timber\Post $post    The post object.
925
         */
926
        return \apply_filters('timber/post/authors', [$this->author()], $this);
5✔
927
    }
928

929
    /**
930
     * Get the author (WordPress user) who last modified the post
931
     *
932
     * @api
933
     * @example
934
     * ```twig
935
     * Last updated by {{ post.modified_author.name }}
936
     * ```
937
     * ```html
938
     * Last updated by Harper Lee
939
     * ```
940
     * @return User|null A User object if found, false if not
941
     */
942
    public function modified_author()
943
    {
944
        $user_id = \get_post_meta($this->ID, '_edit_last', true);
1✔
945
        return ($user_id ? Timber::get_user($user_id) : $this->author());
1✔
946
    }
947

948
    /**
949
     * Get the categories on a particular post
950
     *
951
     * @api
952
     * @return array of Timber\Term objects
953
     */
954
    public function categories()
955
    {
956
        return $this->terms('category');
5✔
957
    }
958

959
    /**
960
     * Gets a category attached to a post.
961
     *
962
     * If multiple categories are set, it will return just the first one.
963
     *
964
     * @api
965
     * @return \Timber\Term|null
966
     */
967
    public function category()
968
    {
969
        $cats = $this->categories();
2✔
970
        if (\count($cats) && isset($cats[0])) {
2✔
971
            return $cats[0];
2✔
972
        }
973

974
        return null;
×
975
    }
976

977
    /**
978
     * Returns an array of children on the post as Timber\Posts
979
     * (or other claass as you define).
980
     *
981
     * @api
982
     * @example
983
     * ```twig
984
     * {% if post.children %}
985
     *     Here are the child pages:
986
     *     {% for child in post.children %}
987
     *         <a href="{{ child.link }}">{{ child.title }}</a>
988
     *     {% endfor %}
989
     * {% endif %}
990
     * ```
991
     * @param string|array $post_type _optional_ use to find children of a particular post type (attachment vs. page for example). You might want to restrict to certain types of children in case other stuff gets all mucked in there. You can use 'parent' to use the parent's post type or you can pass an array of post types.
992
     * @return \Timber\PostCollectionInterface
993
     */
994
    public function children($post_type = 'any')
995
    {
996
        if ($post_type === 'parent') {
4✔
997
            $post_type = $this->post_type;
1✔
998
        }
999
        if (\is_array($post_type)) {
4✔
1000
            $post_type = \implode('&post_type[]=', $post_type);
1✔
1001
        }
1002
        $query = 'post_parent=' . $this->ID . '&post_type[]=' . $post_type . '&posts_per_page=-1&orderby=menu_order title&order=ASC&post_status[]=publish';
4✔
1003
        if ($this->post_status === 'publish') {
4✔
1004
            $query .= '&post_status[]=inherit';
4✔
1005
        }
1006

1007
        return $this->factory()->from(\get_children($query));
4✔
1008
    }
1009

1010
    /**
1011
     * Gets the comments on a Timber\Post and returns them as an array of `Timber\Comment` objects (or whatever comment class you set).
1012
     *
1013
     * @api
1014
     * Gets the comments on a `Timber\Post` and returns them as a `Timber\CommentThread`: a PHP
1015
     * ArrayObject of [`Timber\Comment`](https://timber.github.io/docs/reference/timber-comment/)
1016
     * (or whatever comment class you set).
1017
     * @api
1018
     *
1019
     * @param int    $count        Set the number of comments you want to get. `0` is analogous to
1020
     *                             "all".
1021
     * @param string $order        Use ordering set in WordPress admin, or a different scheme.
1022
     * @param string $type         For when other plugins use the comments table for their own
1023
     *                             special purposes. Might be set to 'liveblog' or other, depending
1024
     *                             on what’s stored in your comments table.
1025
     * @param string $status       Could be 'pending', etc.
1026
     * @see \Timber\CommentThread for an example with nested comments
1027
     * @return bool|\Timber\CommentThread
1028
     *
1029
     * @example
1030
     *
1031
     * **single.twig**
1032
     *
1033
     * ```twig
1034
     * <div id="post-comments">
1035
     *   <h4>Comments on {{ post.title }}</h4>
1036
     *   <ul>
1037
     *     {% for comment in post.comments() %}
1038
     *       {% include 'comment.twig' %}
1039
     *     {% endfor %}
1040
     *   </ul>
1041
     *   <div class="comment-form">
1042
     *     {{ function('comment_form') }}
1043
     *   </div>
1044
     * </div>
1045
     * ```
1046
     *
1047
     * **comment.twig**
1048
     *
1049
     * ```twig
1050
     * {# comment.twig #}
1051
     * <li>
1052
     *   <p class="comment-author">{{ comment.author.name }} says:</p>
1053
     *   <div>{{ comment.content }}</div>
1054
     * </li>
1055
     * ```
1056
     */
1057
    public function comments($count = null, $order = 'wp', $type = 'comment', $status = 'approve')
1058
    {
1059
        global $overridden_cpage, $user_ID;
1060
        $overridden_cpage = false;
13✔
1061

1062
        $commenter = \wp_get_current_commenter();
13✔
1063
        $comment_author_email = $commenter['comment_author_email'];
13✔
1064

1065
        $args = [
13✔
1066
            'status' => $status,
13✔
1067
            'order' => $order,
13✔
1068
            'type' => $type,
13✔
1069
        ];
13✔
1070
        if ($count > 0) {
13✔
1071
            $args['number'] = $count;
1✔
1072
        }
1073
        if (\strtolower($order) == 'wp' || \strtolower($order) == 'wordpress') {
13✔
1074
            $args['order'] = \get_option('comment_order');
13✔
1075
        }
1076
        if ($user_ID) {
13✔
1077
            $args['include_unapproved'] = [$user_ID];
1✔
1078
        } elseif (!empty($comment_author_email)) {
13✔
1079
            $args['include_unapproved'] = [$comment_author_email];
1✔
1080
        } elseif (\function_exists('wp_get_unapproved_comment_author_email')) {
12✔
1081
            $unapproved_email = \wp_get_unapproved_comment_author_email();
12✔
1082
            if ($unapproved_email) {
12✔
1083
                $args['include_unapproved'] = [$unapproved_email];
1✔
1084
            }
1085
        }
1086
        $ct = new CommentThread($this->ID, false);
13✔
1087
        $ct->init($args);
13✔
1088
        return $ct;
13✔
1089
    }
1090

1091
    /**
1092
     * If the Password form is to be shown, show it!
1093
     * @return string|void
1094
     */
1095
    protected function maybe_show_password_form()
1096
    {
1097
        if ($this->password_required()) {
37✔
1098
            $show_pw = false;
3✔
1099

1100
            /**
1101
             * Filters whether the password form should be shown for password protected posts.
1102
             *
1103
             * This filter runs only when you call `{{ post.content }}` for a password protected
1104
             * post. When this filter returns `true`, a password form will be shown instead of the
1105
             * post content. If you want to modify the form itself, you can use the
1106
             * `timber/post/content/password_form` filter.
1107
             *
1108
             * @since 1.1.4
1109
             * @example
1110
             * ```php
1111
             * // Always show password form for password protected posts.
1112
             * add_filter( 'timber/post/content/show_password_form_for_protected', '__return_true' );
1113
             * ```
1114
             *
1115
             * @param bool $show_pw Whether the password form should be shown. Default `false`.
1116
             */
1117
            $show_pw = \apply_filters('timber/post/content/show_password_form_for_protected', $show_pw);
3✔
1118

1119
            if ($show_pw) {
3✔
1120
                /**
1121
                 * Filters the password form output.
1122
                 *
1123
                 * As an alternative to this filter, you could also use WordPress’s `the_password_form` filter.
1124
                 * The difference to this filter is, that you’ll also have the post object available as a second
1125
                 * parameter, in case you need that.
1126
                 *
1127
                 * @since 1.1.4
1128
                 *
1129
                 * @example
1130
                 * ```php
1131
                 * // Modify the password form.
1132
                 * add_filter( 'timber/post/content/password_form', function( $form, $post ) {
1133
                 *     return Timber::compile( 'assets/password-form.twig', array( 'post' => $post ) );
1134
                 * }, 10, 2 );
1135
                 * ```
1136
                 *
1137
                 * @param string       $form Form output. Default WordPress password form output generated by `get_the_password_form()`.
1138
                 * @param \Timber\Post $post The post object.
1139
                 */
1140
                return \apply_filters('timber/post/content/password_form', \get_the_password_form($this->ID), $this);
2✔
1141
            }
1142
        }
1143
    }
1144

1145
    /**
1146
     *
1147
     */
1148
    protected function get_revised_data_from_method($method, $args = false)
1149
    {
1150
        if (!\is_array($args)) {
204✔
1151
            $args = [$args];
169✔
1152
        }
1153
        $rev = $this->get_post_preview_object();
204✔
1154
        if ($rev && $this->ID == $rev->post_parent && $this->ID != $rev->ID) {
204✔
1155
            return \call_user_func_array([$rev, $method], $args);
8✔
1156
        }
1157
    }
1158

1159
    /**
1160
     * Gets the actual content of a WordPress post.
1161
     *
1162
     * As opposed to using `{{ post.post_content }}`, this will run the hooks/filters attached to
1163
     * the `the_content` filter. It will return your post’s content with WordPress filters run on it
1164
     * – which means it will parse blocks, convert shortcodes or run `wpautop()` on the content.
1165
     *
1166
     * If you use page breaks in your content to split your post content into multiple pages,
1167
     * use `{{ post.paged_content }}` to display only the content for the current page.
1168
     *
1169
     * @api
1170
     * @example
1171
     * ```twig
1172
     * <article>
1173
     *     <h1>{{ post.title }}</h1>
1174
     *
1175
     *     <div class="content">{{ post.content }}</div>
1176
     * </article>
1177
     * ```
1178
     *
1179
     * @param int $page Optional. The page to show if the content of the post is split into multiple
1180
     *                  pages. Read more about this in the [Pagination Guide](https://timber.github.io/docs/v2/guides/pagination/#paged-content-within-a-post). Default `0`.
1181
     *
1182
     * @return string
1183
     */
1184
    public function content($page = 0, $len = -1)
1185
    {
1186
        if ($rd = $this->get_revised_data_from_method('content', [$page, $len])) {
37✔
1187
            return $rd;
4✔
1188
        }
1189
        if ($form = $this->maybe_show_password_form()) {
37✔
1190
            return $form;
2✔
1191
        }
1192
        if ($len == -1 && $page == 0 && $this->___content) {
35✔
1193
            return $this->___content;
1✔
1194
        }
1195

1196
        $content = $this->post_content;
35✔
1197

1198
        if ($len > 0) {
35✔
1199
            $content = \wp_trim_words($content, $len);
1✔
1200
        }
1201

1202
        /**
1203
         * Page content split by <!--nextpage-->.
1204
         *
1205
         * @see WP_Query::generate_postdata()
1206
         */
1207
        if ($page && false !== \strpos($content, '<!--nextpage-->')) {
35✔
1208
            $content = \str_replace("\n<!--nextpage-->\n", '<!--nextpage-->', $content);
6✔
1209
            $content = \str_replace("\n<!--nextpage-->", '<!--nextpage-->', $content);
6✔
1210
            $content = \str_replace("<!--nextpage-->\n", '<!--nextpage-->', $content);
6✔
1211

1212
            // Remove the nextpage block delimiters, to avoid invalid block structures in the split content.
1213
            $content = \str_replace('<!-- wp:nextpage -->', '', $content);
6✔
1214
            $content = \str_replace('<!-- /wp:nextpage -->', '', $content);
6✔
1215

1216
            // Ignore nextpage at the beginning of the content.
1217
            if (0 === \strpos($content, '<!--nextpage-->')) {
6✔
1218
                $content = \substr($content, 15);
1✔
1219
            }
1220

1221
            $pages = \explode('<!--nextpage-->', $content);
6✔
1222
            $page--;
6✔
1223

1224
            if (\count($pages) > $page) {
6✔
1225
                $content = $pages[$page];
6✔
1226
            }
1227
        }
1228

1229
        $content = $this->content_handle_no_teaser_block($content);
35✔
1230
        $content = \apply_filters('the_content', ($content));
35✔
1231

1232
        if ($len == -1 && $page == 0) {
35✔
1233
            $this->___content = $content;
34✔
1234
        }
1235

1236
        return $content;
35✔
1237
    }
1238

1239
    /**
1240
     * Handles for an circumstance with the Block editor where a "more" block has an option to
1241
     * "Hide the excerpt on the full content page" which hides everything prior to the inserted
1242
     * "more" block
1243
     * @ticket #2218
1244
     * @param string $content
1245
     * @return string
1246
     */
1247
    protected function content_handle_no_teaser_block($content)
1248
    {
1249
        if ((\strpos($content, 'noTeaser:true') !== false || \strpos($content, '"noTeaser":true') !== false) && \strpos($content, '<!-- /wp:more -->') !== false) {
35✔
1250
            $arr = \explode('<!-- /wp:more -->', $content);
1✔
1251
            return \trim($arr[1]);
1✔
1252
        }
1253
        return $content;
34✔
1254
    }
1255

1256
    /**
1257
     * Gets the paged content for a post.
1258
     *
1259
     * You will use this, if you use `<!--nextpage-->` in your post content or the Page Break block
1260
     * in the Block Editor. Use `{{ post.pagination }}` to create a pagination for your paged
1261
     * content. Learn more about this in the [Pagination Guide](https://timber.github.io/docs/v2/guides/pagination/#paged-content-within-a-post).
1262
     *
1263
     * @example
1264
     * ```twig
1265
     * {{ post.paged_content }}
1266
     * ```
1267
     *
1268
     * @return string The content for the current page. If there’s no page break found in the
1269
     *                content, the whole content is returned.
1270
     */
1271
    public function paged_content()
1272
    {
1273
        global $page;
1274
        return $this->content($page, -1);
4✔
1275
    }
1276

1277
    /**
1278
     * Gets the timestamp when the post was published.
1279
     *
1280
     * @api
1281
     * @since 2.0.0
1282
     *
1283
     * @return false|int Unix timestamp on success, false on failure.
1284
     */
1285
    public function timestamp()
1286
    {
1287
        return \get_post_timestamp($this->ID);
10✔
1288
    }
1289

1290
    /**
1291
     * Gets the timestamp when the post was last modified.
1292
     *
1293
     * @api
1294
     * @since 2.0.0
1295
     *
1296
     * @return false|int Unix timestamp on success, false on failure.
1297
     */
1298
    public function modified_timestamp()
1299
    {
1300
        return \get_post_timestamp($this->ID, 'modified');
4✔
1301
    }
1302

1303
    /**
1304
     * Gets the publishing date of the post.
1305
     *
1306
     * This function will also apply the
1307
     * [`get_the_date`](https://developer.wordpress.org/reference/hooks/get_the_date/) filter to the
1308
     * output.
1309
     *
1310
     * If you use {{ post.date }} with the |time_ago filter, then make sure that you use a time
1311
     * format including the full time and not just the date.
1312
     *
1313
     * @api
1314
     * @example
1315
     * ```twig
1316
     * {# Uses date format set in Settings → General #}
1317
     * Published on {{ post.date }}
1318
     * OR
1319
     * Published on {{ post.date('F jS') }}
1320
     * which was
1321
     * {{ post.date('U')|time_ago }}
1322
     * {{ post.date('Y-m-d H:i:s')|time_ago }}
1323
     * {{ post.date(constant('DATE_ATOM'))|time_ago }}
1324
     * ```
1325
     *
1326
     * ```html
1327
     * Published on January 12, 2015
1328
     * OR
1329
     * Published on Jan 12th
1330
     * which was
1331
     * 8 years ago
1332
     * ```
1333
     *
1334
     * @param string|null $date_format Optional. PHP date format. Will use the `date_format` option
1335
     *                                 as a default.
1336
     *
1337
     * @return string
1338
     */
1339
    public function date($date_format = null)
1340
    {
1341
        $format = $date_format ?: \get_option('date_format');
8✔
1342
        $date = \wp_date($format, $this->timestamp());
8✔
1343

1344
        /**
1345
         * Filters the date a post was published.
1346
         *
1347
         * @see get_the_date()
1348
         *
1349
         * @param string      $date        The formatted date.
1350
         * @param string      $date_format PHP date format. Defaults to 'date_format' option if not
1351
         *                                 specified.
1352
         * @param int|WP_Post $id          The post object or ID.
1353
         */
1354
        $date = \apply_filters('get_the_date', $date, $date_format, $this->ID);
8✔
1355

1356
        return $date;
8✔
1357
    }
1358

1359
    /**
1360
     * Gets the date the post was last modified.
1361
     *
1362
     * This function will also apply the
1363
     * [`get_the_modified_date`](https://developer.wordpress.org/reference/hooks/get_the_modified_date/)
1364
     * filter to the output.
1365
     *
1366
     * @api
1367
     * @example
1368
     * ```twig
1369
     * {# Uses date format set in Settings → General #}
1370
     * Last modified on {{ post.modified_date }}
1371
     * OR
1372
     * Last modified on {{ post.modified_date('F jS') }}
1373
     * ```
1374
     *
1375
     * ```html
1376
     * Last modified on January 12, 2015
1377
     * OR
1378
     * Last modified on Jan 12th
1379
     * ```
1380
     *
1381
     * @param string|null $date_format Optional. PHP date format. Will use the `date_format` option
1382
     *                                 as a default.
1383
     *
1384
     * @return string
1385
     */
1386
    public function modified_date($date_format = null)
1387
    {
1388
        $format = $date_format ?: \get_option('date_format');
2✔
1389
        $date = \wp_date($format, $this->modified_timestamp());
2✔
1390

1391
        /**
1392
         * Filters the date a post was last modified.
1393
         *
1394
         * This filter expects a `WP_Post` object as the last parameter. We only have a
1395
         * `Timber\Post` object available, that wouldn’t match the expected argument. That’s why we
1396
         * need to get the post object with get_post(). This is fairly inexpensive, because the post
1397
         * will already be in the cache.
1398
         *
1399
         * @see get_the_modified_date()
1400
         *
1401
         * @param string|bool  $date        The formatted date or false if no post is found.
1402
         * @param string       $date_format PHP date format. Defaults to value specified in
1403
         *                                  'date_format' option.
1404
         * @param WP_Post|null $post        WP_Post object or null if no post is found.
1405
         */
1406
        $date = \apply_filters('get_the_modified_date', $date, $date_format, \get_post($this->ID));
2✔
1407

1408
        return $date;
2✔
1409
    }
1410

1411
    /**
1412
     * Gets the time the post was published to use in your template.
1413
     *
1414
     * This function will also apply the
1415
     * [`get_the_time`](https://developer.wordpress.org/reference/hooks/get_the_time/) filter to the
1416
     * output.
1417
     *
1418
     * @api
1419
     * @example
1420
     * ```twig
1421
     * {# Uses time format set in Settings → General #}
1422
     * Published at {{ post.time }}
1423
     * OR
1424
     * Published at {{ post.time|time('G:i') }}
1425
     * ```
1426
     *
1427
     * ```html
1428
     * Published at 1:25 pm
1429
     * OR
1430
     * Published at 13:25
1431
     * ```
1432
     *
1433
     * @param string|null $time_format Optional. PHP date format. Will use the `time_format` option
1434
     *                                 as a default.
1435
     *
1436
     * @return string
1437
     */
1438
    public function time($time_format = null)
1439
    {
1440
        $format = $time_format ?: \get_option('time_format');
2✔
1441
        $time = \wp_date($format, $this->timestamp());
2✔
1442

1443
        /**
1444
         * Filters the time a post was written.
1445
         *
1446
         * @see get_the_time()
1447
         *
1448
         * @param string      $time        The formatted time.
1449
         * @param string      $time_format Format to use for retrieving the time the post was
1450
         *                                 written. Accepts 'G', 'U', or php date format value
1451
         *                                 specified in `time_format` option. Default empty.
1452
         * @param int|WP_Post $id          WP_Post object or ID.
1453
         */
1454
        $time = \apply_filters('get_the_time', $time, $time_format, $this->ID);
2✔
1455

1456
        return $time;
2✔
1457
    }
1458

1459
    /**
1460
     * Gets the time of the last modification of the post to use in your template.
1461
     *
1462
     * This function will also apply the
1463
     * [`get_the_time`](https://developer.wordpress.org/reference/hooks/get_the_modified_time/)
1464
     * filter to the output.
1465
     *
1466
     * @api
1467
     * @example
1468
     * ```twig
1469
     * {# Uses time format set in Settings → General #}
1470
     * Published at {{ post.time }}
1471
     * OR
1472
     * Published at {{ post.time|time('G:i') }}
1473
     * ```
1474
     *
1475
     * ```html
1476
     * Published at 1:25 pm
1477
     * OR
1478
     * Published at 13:25
1479
     * ```
1480
     *
1481
     * @param string|null $time_format Optional. PHP date format. Will use the `time_format` option
1482
     *                                 as a default.
1483
     *
1484
     * @return string
1485
     */
1486
    public function modified_time($time_format = null)
1487
    {
1488
        $format = $time_format ?: \get_option('time_format');
2✔
1489
        $time = \wp_date($format, $this->modified_timestamp());
2✔
1490

1491
        /**
1492
         * Filters the localized time a post was last modified.
1493
         *
1494
         * This filter expects a `WP_Post` object as the last parameter. We only have a
1495
         * `Timber\Post` object available, that wouldn’t match the expected argument. That’s why we
1496
         * need to get the post object with get_post(). This is fairly inexpensive, because the post
1497
         * will already be in the cache.
1498
         *
1499
         * @see get_the_modified_time()
1500
         *
1501
         * @param string|bool  $time        The formatted time or false if no post is found.
1502
         * @param string       $time_format Format to use for retrieving the time the post was
1503
         *                                  written. Accepts 'G', 'U', or php date format. Defaults
1504
         *                                  to value specified in 'time_format' option.
1505
         * @param WP_Post|null $post        WP_Post object or null if no post is found.
1506
         */
1507
        $time = \apply_filters('get_the_modified_time', $time, $time_format, \get_post($this->ID));
2✔
1508

1509
        return $time;
2✔
1510
    }
1511

1512
    /**
1513
     * Returns the PostType object for a post’s post type with labels and other info.
1514
     *
1515
     * @api
1516
     * @since 1.0.4
1517
     * @example
1518
     * ```twig
1519
     * This post is from <span>{{ post.type.labels.name }}</span>
1520
     * ```
1521
     *
1522
     * ```html
1523
     * This post is from <span>Recipes</span>
1524
     * ```
1525
     * @return \Timber\PostType
1526
     */
1527
    public function type()
1528
    {
1529
        if (!$this->__type instanceof PostType) {
3✔
1530
            $this->__type = new PostType($this->post_type);
3✔
1531
        }
1532
        return $this->__type;
3✔
1533
    }
1534

1535
    /**
1536
     * Checks whether the current user can edit the post.
1537
     *
1538
     * @api
1539
     * @example
1540
     * ```twig
1541
     * {% if post.can_edit %}
1542
     *     <a href="{{ post.edit_link }}">Edit</a>
1543
     * {% endif %}
1544
     * ```
1545
     * @return bool
1546
     */
1547
    public function can_edit(): bool
1548
    {
1549
        return \current_user_can('edit_post', $this->ID);
2✔
1550
    }
1551

1552
    /**
1553
     * Gets the edit link for a post if the current user has the correct rights.
1554
     *
1555
     * @api
1556
     * @example
1557
     * ```twig
1558
     * {% if post.can_edit %}
1559
     *     <a href="{{ post.edit_link }}">Edit</a>
1560
     * {% endif %}
1561
     * ```
1562
     * @return string|null The edit URL of a post in the WordPress admin or null if the current user can’t edit the
1563
     *                     post.
1564
     */
1565
    public function edit_link(): ?string
1566
    {
1567
        if (!$this->can_edit()) {
1✔
1568
            return null;
1✔
1569
        }
1570

1571
        return \get_edit_post_link($this->ID);
1✔
1572
    }
1573

1574
    /**
1575
     * @api
1576
     * @return mixed
1577
     */
1578
    public function format()
1579
    {
1580
        return \get_post_format($this->ID);
1✔
1581
    }
1582

1583
    /**
1584
     * whether post requires password and correct password has been provided
1585
     * @api
1586
     * @return boolean
1587
     */
1588
    public function password_required()
1589
    {
1590
        return \post_password_required($this->ID);
38✔
1591
    }
1592

1593
    /**
1594
     * get the permalink for a post object
1595
     * @api
1596
     * @example
1597
     * ```twig
1598
     * <a href="{{post.link}}">Read my post</a>
1599
     * ```
1600
     * @return string ex: http://example.org/2015/07/my-awesome-post
1601
     */
1602
    public function link()
1603
    {
1604
        if (isset($this->_permalink)) {
32✔
1605
            return $this->_permalink;
14✔
1606
        }
1607
        $this->_permalink = \get_permalink($this->ID);
32✔
1608
        return $this->_permalink;
32✔
1609
    }
1610

1611
    /**
1612
     * @api
1613
     * @return string
1614
     */
1615
    public function name()
1616
    {
1617
        return $this->title();
1✔
1618
    }
1619

1620
    /**
1621
     * Gets the next post that is adjacent to the current post in a collection.
1622
     *
1623
     * Works pretty much the same as
1624
     * [`get_next_post()`](https://developer.wordpress.org/reference/functions/get_next_post/).
1625
     *
1626
     * @api
1627
     * @example
1628
     * ```twig
1629
     * {% if post.next %}
1630
     *     <a href="{{ post.next.link }}">{{ post.next.title }}</a>
1631
     * {% endif %}
1632
     * ```
1633
     * @param bool|string $in_same_term Whether the post should be in a same taxonomy term. Default
1634
     *                                  `false`.
1635
     *
1636
     * @return mixed
1637
     */
1638
    public function next($in_same_term = false)
1639
    {
1640
        if (!isset($this->_next) || !isset($this->_next[$in_same_term])) {
5✔
1641
            global $post;
1642
            $this->_next = [];
5✔
1643
            $old_global = $post;
5✔
1644
            $post = $this;
5✔
1645
            if (\is_string($in_same_term) && \strlen($in_same_term)) {
5✔
1646
                $adjacent = \get_adjacent_post(true, '', false, $in_same_term);
2✔
1647
            } else {
1648
                $adjacent = \get_adjacent_post(false, '', false);
3✔
1649
            }
1650

1651
            if ($adjacent) {
5✔
1652
                $this->_next[$in_same_term] = $this->factory()->from($adjacent);
4✔
1653
            } else {
1654
                $this->_next[$in_same_term] = false;
1✔
1655
            }
1656
            $post = $old_global;
5✔
1657
        }
1658
        return $this->_next[$in_same_term];
5✔
1659
    }
1660

1661
    /**
1662
     * Gets a data array to display a pagination for your paginated post.
1663
     *
1664
     * Use this in combination with `{{ post.paged_content }}`.
1665
     *
1666
     * @api
1667
     * @example
1668
     * Using simple links to the next an previous page.
1669
     * ```twig
1670
     * {% if post.pagination.next is not empty %}
1671
     *     <a href="{{ post.pagination.next.link|e('esc_url') }}">Go to next page</a>
1672
     * {% endif %}
1673
     *
1674
     * {% if post.pagination.prev is not empty %}
1675
     *     <a href="{{ post.pagination.prev.link|e('esc_url') }}">Go to previous page</a>
1676
     * {% endif %}
1677
     * ```
1678
     * Using a pagination for all pages.
1679
     * ```twig
1680
     * {% if post.pagination.pages is not empty %}
1681
     *    <nav aria-label="pagination">
1682
     *        <ul>
1683
     *            {% for page in post.pagination.pages %}
1684
     *                <li>
1685
     *                    {% if page.current %}
1686
     *                        <span aria-current="page">Page {{ page.title }}</span>
1687
     *                    {% else %}
1688
     *                        <a href="{{ page.link|e('esc_url') }}">Page {{ page.title }}</a>
1689
     *                    {% endif %}
1690
     *                </li>
1691
     *            {% endfor %}
1692
     *        </ul>
1693
     *    </nav>
1694
     * {% endif %}
1695
     * ```
1696
     *
1697
     * @return array An array with data to build your paginated content.
1698
     */
1699
    public function pagination()
1700
    {
1701
        global $post, $page, $numpages, $multipage;
1702
        $post = $this;
4✔
1703
        $ret = [];
4✔
1704
        if ($multipage) {
4✔
1705
            for ($i = 1; $i <= $numpages; $i++) {
4✔
1706
                $link = self::get_wp_link_page($i);
4✔
1707
                $data = [
4✔
1708
                    'name' => $i,
4✔
1709
                    'title' => $i,
4✔
1710
                    'text' => $i,
4✔
1711
                    'link' => $link,
4✔
1712
                ];
4✔
1713
                if ($i == $page) {
4✔
1714
                    $data['current'] = true;
4✔
1715
                }
1716
                $ret['pages'][] = $data;
4✔
1717
            }
1718
            $i = $page - 1;
4✔
1719
            if ($i) {
4✔
1720
                $link = self::get_wp_link_page($i);
×
1721
                $ret['prev'] = [
×
1722
                    'link' => $link,
×
1723
                ];
×
1724
            }
1725
            $i = $page + 1;
4✔
1726
            if ($i <= $numpages) {
4✔
1727
                $link = self::get_wp_link_page($i);
4✔
1728
                $ret['next'] = [
4✔
1729
                    'link' => $link,
4✔
1730
                ];
4✔
1731
            }
1732
        }
1733
        return $ret;
4✔
1734
    }
1735

1736
    /**
1737
     * Finds any WP_Post objects and converts them to Timber\Post objects.
1738
     *
1739
     * @api
1740
     * @param array|WP_Post $data
1741
     */
1742
    public function convert($data)
1743
    {
1744
        if (\is_object($data)) {
17✔
1745
            $data = Helper::convert_wp_object($data);
15✔
1746
        } elseif (\is_array($data)) {
7✔
1747
            $data = \array_map([$this, 'convert'], $data);
6✔
1748
        }
1749
        return $data;
17✔
1750
    }
1751

1752
    /**
1753
     * Gets the parent (if one exists) from a post as a Timber\Post object.
1754
     * Honors Class Maps.
1755
     *
1756
     * @api
1757
     * @example
1758
     * ```twig
1759
     * Parent page: <a href="{{ post.parent.link }}">{{ post.parent.title }}</a>
1760
     * ```
1761
     * @return bool|\Timber\Post
1762
     */
1763
    public function parent()
1764
    {
1765
        if (!$this->post_parent) {
3✔
1766
            return false;
1✔
1767
        }
1768

1769
        return $this->factory()->from($this->post_parent);
2✔
1770
    }
1771

1772
    /**
1773
     * Gets the relative path of a WP Post, so while link() will return http://example.org/2015/07/my-cool-post
1774
     * this will return just /2015/07/my-cool-post
1775
     *
1776
     * @api
1777
     * @example
1778
     * ```twig
1779
     * <a href="{{post.path}}">{{post.title}}</a>
1780
     * ```
1781
     * @return string
1782
     */
1783
    public function path()
1784
    {
1785
        return URLHelper::get_rel_url($this->link());
3✔
1786
    }
1787

1788
    /**
1789
     * Get the previous post that is adjacent to the current post in a collection.
1790
     *
1791
     * Works pretty much the same as
1792
     * [`get_previous_post()`](https://developer.wordpress.org/reference/functions/get_previous_post/).
1793
     *
1794
     * @api
1795
     * @example
1796
     * ```twig
1797
     * {% if post.prev %}
1798
     *     <a href="{{ post.prev.link }}">{{ post.prev.title }}</a>
1799
     * {% endif %}
1800
     * ```
1801
     * @param bool|string $in_same_term Whether the post should be in a same taxonomy term. Default
1802
     *                                  `false`.
1803
     * @return mixed
1804
     */
1805
    public function prev($in_same_term = false)
1806
    {
1807
        if (isset($this->_prev) && isset($this->_prev[$in_same_term])) {
3✔
1808
            return $this->_prev[$in_same_term];
×
1809
        }
1810
        global $post;
1811
        $old_global = $post;
3✔
1812
        $post = $this;
3✔
1813
        $within_taxonomy = ($in_same_term) ? $in_same_term : 'category';
3✔
1814
        $adjacent = \get_adjacent_post(($in_same_term), '', true, $within_taxonomy);
3✔
1815
        $prev_in_taxonomy = false;
3✔
1816
        if ($adjacent) {
3✔
1817
            $prev_in_taxonomy = $this->factory()->from($adjacent);
3✔
1818
        }
1819
        $this->_prev[$in_same_term] = $prev_in_taxonomy;
3✔
1820
        $post = $old_global;
3✔
1821
        return $this->_prev[$in_same_term];
3✔
1822
    }
1823

1824
    /**
1825
     * Gets the tags on a post, uses WP's post_tag taxonomy
1826
     *
1827
     * @api
1828
     * @return array
1829
     */
1830
    public function tags()
1831
    {
1832
        return $this->terms('post_tag');
3✔
1833
    }
1834

1835
    /**
1836
     * Gets the post’s thumbnail ID.
1837
     *
1838
     * @api
1839
     * @since 2.0.0
1840
     *
1841
     * @return false|int The default post’s ID. False if no thumbnail was defined.
1842
     */
1843
    public function thumbnail_id()
1844
    {
1845
        return (int) \get_post_meta($this->ID, '_thumbnail_id', true);
25✔
1846
    }
1847

1848
    /**
1849
     * get the featured image as a Timber/Image
1850
     *
1851
     * @api
1852
     * @example
1853
     * ```twig
1854
     * <img src="{{ post.thumbnail.src }}" />
1855
     * ```
1856
     * @return \Timber\Image|null of your thumbnail
1857
     */
1858
    public function thumbnail()
1859
    {
1860
        $tid = $this->thumbnail_id();
24✔
1861

1862
        if ($tid) {
24✔
1863
            return $this->factory()->from($tid);
23✔
1864
        }
1865

1866
        return null;
1✔
1867
    }
1868

1869
    /**
1870
     * 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.
1871
     *
1872
     * @api
1873
     * @example
1874
     * ```twig
1875
     * <h1>{{ post.title }}</h1>
1876
     * ```
1877
     * @return string
1878
     */
1879
    public function title()
1880
    {
1881
        if ($rd = $this->get_revised_data_from_method('title')) {
50✔
1882
            return $rd;
1✔
1883
        }
1884
        return \apply_filters('the_title', $this->post_title, $this->ID);
50✔
1885
    }
1886

1887
    /**
1888
     * Returns galleries from the post’s content.
1889
     *
1890
     * @api
1891
     * @example
1892
     * ```twig
1893
     * {{ post.gallery }}
1894
     * ```
1895
     * @return array A list of arrays, each containing gallery data and srcs parsed from the
1896
     * expanded shortcode.
1897
     */
1898
    public function gallery($html = true)
1899
    {
1900
        $galleries = \get_post_galleries($this->ID, $html);
1✔
1901
        $gallery = \reset($galleries);
1✔
1902

1903
        return \apply_filters('get_post_gallery', $gallery, $this->ID, $galleries);
1✔
1904
    }
1905

1906
    protected function get_entity_name()
1907
    {
1908
        return 'post';
×
1909
    }
1910

1911
    /**
1912
     * Given a base query and a list of taxonomies, return a list of queries
1913
     * each of which queries for one of the taxonomies.
1914
     * @example
1915
     * ```
1916
     * $this->partition_tax_queries(["object_ids" => [123]], ["a", "b"]);
1917
     *
1918
     * // result:
1919
     * // [
1920
     * //   ["object_ids" => [123], "taxonomy" => ["a"]],
1921
     * //   ["object_ids" => [123], "taxonomy" => ["b"]],
1922
     * // ]
1923
     * ```
1924
     * @internal
1925
     */
1926
    private function partition_tax_queries(array $query, array $taxonomies): array
1927
    {
1928
        return \array_map(function (string $tax) use ($query): array {
3✔
1929
            return \array_merge($query, [
3✔
1930
                'taxonomy' => [$tax],
3✔
1931
            ]);
3✔
1932
        }, $taxonomies);
3✔
1933
    }
1934

1935
    /**
1936
     * Get a PostFactory instance for internal usage
1937
     *
1938
     * @internal
1939
     * @return \Timber\Factory\PostFactory
1940
     */
1941
    private function factory()
1942
    {
1943
        static $factory;
36✔
1944
        $factory = $factory ?: new PostFactory();
36✔
1945
        return $factory;
36✔
1946
    }
1947
}
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