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

timber / timber / 20695674007

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

push

travis-ci

nlemoine
test: Fix ancestors post tests

4615 of 5146 relevant lines covered (89.68%)

63.45 hits per line

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

85.89
/src/Timber.php
1
<?php
2

3
namespace Timber;
4

5
use Timber\Factory\CommentFactory;
6
use Timber\Factory\MenuFactory;
7
use Timber\Factory\PagesMenuFactory;
8
use Timber\Factory\PostFactory;
9
use Timber\Factory\TermFactory;
10
use Timber\Factory\UserFactory;
11
use Timber\Integration\IntegrationInterface;
12
use WP_Comment;
13
use WP_Comment_Query;
14
use WP_Post;
15
use WP_Query;
16
use WP_Term;
17
use WP_User;
18

19
/**
20
 * Class Timber
21
 *
22
 * Main class called Timber for this plugin.
23
 *
24
 * @api
25
 * @example
26
 * ```php
27
 * // Get default posts on an archive page
28
 * $posts = Timber::get_posts();
29
 *
30
 * // Query for some posts
31
 * $posts = Timber::get_posts( [
32
 *     'post_type' => 'article',
33
 *     'category_name' => 'sports',
34
 * ] );
35
 *
36
 * $context = Timber::context( [
37
 *     'posts' => $posts,
38
 * ] );
39
 *
40
 * Timber::render( 'index.twig', $context );
41
 * ```
42
 */
43
class Timber
44
{
45
    public static $version = '2.3.3'; // x-release-please-version
46

47
    public static $locations;
48

49
    public static $dirname = 'views';
50

51
    public static $auto_meta = true;
52

53
    /**
54
     * Global context cache.
55
     *
56
     * @var array An array containing global context variables.
57
     */
58
    public static $context_cache = [];
59

60
    /**
61
     * Caching option for Twig.
62
     *
63
     * @deprecated 2.0.0
64
     * @var bool
65
     */
66
    public static $twig_cache = false;
67

68
    /**
69
     * Caching option for Twig.
70
     *
71
     * Alias for `Timber::$twig_cache`.
72
     *
73
     * @deprecated 2.0.0
74
     * @var bool
75
     */
76
    public static $cache = false;
77

78
    /**
79
     * Autoescaping option for Twig.
80
     *
81
     * @deprecated 2.0.0
82
     * @var bool
83
     */
84
    public static $autoescape = false;
85

86
    /**
87
     * Timber should be loaded with Timber\Timber::init() and not new Timber\Timber();
88
     *
89
     * @codeCoverageIgnore
90
     */
91
    protected function __construct()
92
    {
93
    }
94

95
    protected function init_constants()
×
96
    {
97
        \defined("TIMBER_LOC") or \define("TIMBER_LOC", \realpath(\dirname(__DIR__)));
×
98
    }
99

100
    /**
101
     * @codeCoverageIgnore
102
     */
103
    public static function init(): void
104
    {
105
        if (
106
            !\defined('ABSPATH')
107
            || !\class_exists('\WP')
108
            || \defined('TIMBER_LOADED')
109
        ) {
110
            return;
111
        }
112

113
        $self = new self();
114
        $self->init_constants();
115

116
        Twig::init();
117
        ImageHelper::init();
118

119
        \add_action('init', [self::class, 'init_integrations']);
120
        \add_action('admin_init', [Admin::class, 'init']);
121

122
        \add_filter('timber/post/import_data', [self::class, 'handle_preview'], 10, 2);
123

124
        /**
125
         * Make an alias for the Timber class.
126
         *
127
         * This way, developers can use Timber::render() instead of Timber\Timber::render, which
128
         * is more user-friendly.
129
         */
130
        \class_alias(Timber::class, 'Timber');
131

132
        \define('TIMBER_LOADED', true);
133
    }
134

135
    /**
136
     * Initializes Timber's integrations.
137
     *
138
     * @return void
139
     */
140
    public static function init_integrations(): void
×
141
    {
142
        $integrations = [
×
143
            new Integration\AcfIntegration(),
×
144
            new Integration\CoAuthorsPlusIntegration(),
×
145
            new Integration\WpCliIntegration(),
×
146
            new Integration\WpmlIntegration(),
×
147
        ];
×
148

149
        /**
150
         * Filters the integrations that should be initialized by Timber.
151
         *
152
         * @since 2.0.0
153
         *
154
         * @param IntegrationInterface[] $integrations An array of PHP class names. Default: array of
155
         *                            integrations that Timber initializes by default.
156
         */
157
        $integrations = \apply_filters('timber/integrations', $integrations);
×
158

159
        // Integration classes must implement the IntegrationInterface.
160
        $integrations = \array_filter($integrations, static fn ($integration) => $integration instanceof IntegrationInterface);
×
161

162
        foreach ($integrations as $integration) {
×
163
            if (!$integration->should_init()) {
×
164
                continue;
×
165
            }
166
            $integration->init();
×
167
        }
168
    }
169

170
    /**
171
     * Handles previewing posts.
172
     *
173
     * @param array $data
174
     * @param Post $post
175
     * @return array
176
     */
177
    public static function handle_preview($data, $post)
435✔
178
    {
179
        if (!isset($_GET['preview']) || !isset($_GET['preview_id'])) {
435✔
180
            return $data;
433✔
181
        }
182

183
        $preview_post_id = (int) $_GET['preview_id'];
2✔
184
        $current_post_id = $post->ID ?? null;
2✔
185
        // ⚠️ Don't filter imported data if the current post ID is not the preview post ID.
186
        // You might alter every `Timber::get_post()`!
187
        if ($current_post_id !== $preview_post_id) {
2✔
188
            return $data;
×
189
        }
190

191
        $preview = \wp_get_post_autosave($preview_post_id);
2✔
192

193
        if (\is_object($preview)) {
2✔
194
            $preview = \sanitize_post($preview);
2✔
195

196
            $data['post_content'] = $preview->post_content;
2✔
197
            $data['post_title'] = $preview->post_title;
2✔
198
            $data['post_excerpt'] = $preview->post_excerpt;
2✔
199

200
            \add_filter('get_the_terms', '_wp_preview_terms_filter', 10, 3);
2✔
201
        }
202

203
        return $data;
2✔
204
    }
205

206
    /* Post Retrieval Routine
207
    ================================ */
208

209
    /**
210
     * Gets a Timber Post from a post ID, WP_Post object, a WP_Query object, or an associative
211
     * array of arguments for WP_Query::__construct().
212
     *
213
     * By default, Timber will use the `Timber\Post` class to create a new post object. To control
214
     * which class is instantiated for your Post object, use [Class Maps](https://timber.github.io/docs/v2/guides/class-maps/)
215
     *
216
     * @api
217
     * @example
218
     * ```php
219
     * // Using a post ID.
220
     * $post = Timber::get_post( 75 );
221
     *
222
     * // Using a WP_Post object.
223
     * $wp_post = get_post( 123 );
224
     * $post    = Timber::get_post( $wp_post );
225
     *
226
     * // Using a WP_Query argument array
227
     * $post = Timber::get_post( [
228
     *   'post_type' => 'page',
229
     * ] );
230
     *
231
     * // Use currently queried post. Same as using get_the_ID() as a parameter.
232
     * $post = Timber::get_post();
233
     *
234
     * // From an associative array with an `ID` key. For ACF compatibility. Using this
235
     * // approach directly is not recommended. If you can, configure the return type of your
236
     * // ACF field to just the ID.
237
     * $post = Timber::get_post( get_field('associated_post_array') ); // Just OK.
238
     * $post = Timber::get_post( get_field('associated_post_id') ); // Better!
239
     * ```
240
     * @see https://developer.wordpress.org/reference/classes/wp_query/__construct/
241
     *
242
     * @param mixed $query   Optional. Post ID or query (as an array of arguments for WP_Query).
243
     *                              If a query is provided, only the first post of the result will be
244
     *                       returned. Default false.
245
     * @param array $options Optional associative array of options. Defaults to an empty array.
246
     *
247
     * @return Post|null Timber\Post object if a post was found, null if no post was
248
     *                           found.
249
     */
250
    public static function get_post(mixed $query = false, $options = [])
376✔
251
    {
252
        self::check_post_api_deprecations($query, $options, 'Timber::get_post()');
376✔
253

254
        if (\is_string($options)) {
376✔
255
            $options = [];
1✔
256
        }
257

258
        $factory = new PostFactory();
376✔
259

260
        global $wp_query;
261

262
        $options = \wp_parse_args($options, [
376✔
263
            'merge_default' => false,
376✔
264
        ]);
376✔
265

266
        // Has WP already queried and found a post?
267
        if ($query === false && ($wp_query->queried_object instanceof WP_Post)) {
376✔
268
            $query = $wp_query->queried_object;
16✔
269
        } elseif (\is_array($query) && $options['merge_default']) {
365✔
270
            $query = \wp_parse_args($wp_query->query_vars);
1✔
271
        }
272

273
        // Default to the global query.
274
        $result = $factory->from($query ?: $wp_query);
376✔
275

276
        // If we got a Collection, return the first Post.
277
        if ($result instanceof PostCollectionInterface) {
376✔
278
            return $result[0] ?? null;
16✔
279
        }
280

281
        return $result;
361✔
282
    }
283

284
    /**
285
     * Gets an attachment.
286
     *
287
     * Behaves just like Timber::get_post(), except that it returns null if it finds a Timber\Post
288
     * that is not an Attachment. Honors Class Maps and falsifies return value *after* Class Map for
289
     * the found Timber\Post has been resolved.
290
     *
291
     * @api
292
     * @since 2.0.0
293
     * @see Timber::get_post()
294
     * @see https://timber.github.io/docs/v2/guides/class-maps/
295
     *
296
     * @param mixed $query   Optional. Query or post identifier. Default false.
297
     * @param array $options Optional. Options for Timber\Timber::get_post().
298
     *
299
     * @return Attachment|null Timber\Attachment object if an attachment was found, null if no
300
     *                         attachment was found.
301
     */
302
    public static function get_attachment(mixed $query = false, $options = [])
2✔
303
    {
304
        self::check_post_api_deprecations($query, $options, 'Timber::get_attachment()');
2✔
305

306
        $post = static::get_post($query, $options);
2✔
307

308
        // No need to instantiate a Post we're not going to use.
309
        return ($post instanceof Attachment) ? $post : null;
2✔
310
    }
311

312
    /**
313
     * Gets an image.
314
     *
315
     * Behaves just like Timber::get_post(), except that it returns null if it finds a Timber\Post
316
     * that is not an Image. Honors Class Maps and falsifies return value *after* Class Map for the
317
     * found Timber\Post has been resolved.
318
     *
319
     * @api
320
     * @since 2.0.0
321
     * @see Timber::get_post()
322
     * @see https://timber.github.io/docs/v2/guides/class-maps/
323
     *
324
     * @param mixed $query   Optional. Query or post identifier. Default false.
325
     * @param array $options Optional. Options for Timber\Timber::get_post().
326
     *
327
     * @return Image|null
328
     */
329
    public static function get_image(mixed $query = false, $options = [])
10✔
330
    {
331
        self::check_post_api_deprecations($query, $options, 'Timber::get_image()');
10✔
332

333
        $post = static::get_post($query, $options);
10✔
334

335
        // No need to instantiate a Post we're not going to use.
336
        return ($post instanceof Image) ? $post : null;
10✔
337
    }
338

339
    /**
340
     * Gets an external image.
341
     *
342
     * Behaves just like Timber::get_image(), except that you can use an absolute or relative path or a URL to load an
343
     * image. You can also pass in an external URL. In that case, Timber will sideload the image and store it in the
344
     * uploads folder of your WordPress installation. The next time the image is accessed, it will be loaded from there.
345
     *
346
     * @api
347
     * @since 2.0.0
348
     * @see Timber::get_image()
349
     * @see ImageHelper::sideload_image()
350
     *
351
     * @param bool  $url Image path or URL. The path can be absolute or relative to the WordPress installation.
352
     * @param array $args {
353
     *     An associative array with additional arguments for the image.
354
     *
355
     *     @type string $alt Alt text for the image.
356
     *     @type string $caption Caption text for the image.
357
     * }
358
     *
359
     * @return ExternalImage|null
360
     */
361
    public static function get_external_image($url = false, array $args = []): ?ExternalImage
20✔
362
    {
363
        $args = \wp_parse_args($args, [
20✔
364
            'alt' => '',
20✔
365
            'caption' => '',
20✔
366
        ]);
20✔
367

368
        return ExternalImage::build($url, $args);
20✔
369
    }
370

371
    /**
372
     * Checks for deprecated Timber::get_post() API usage.
373
     *
374
     * @param $query
375
     * @param $options
376
     * @param $function_name
377
     */
378
    private static function check_post_api_deprecations($query = false, $options = [], string $function_name = 'Timber::get_post()')
376✔
379
    {
380
        if (\is_string($query) && !\is_numeric($query)) {
376✔
381
            Helper::doing_it_wrong(
1✔
382
                $function_name,
1✔
383
                'Getting a post by post slug or post name was removed from Timber::get_post() in Timber 2.0. Use Timber::get_post_by() instead.',
1✔
384
                '2.0.0'
1✔
385
            );
1✔
386
        }
387

388
        if (\is_string($options)) {
376✔
389
            Helper::doing_it_wrong(
1✔
390
                $function_name,
1✔
391
                'The $PostClass parameter for passing in the post class to use in Timber::get_posts() was replaced with an $options array in Timber 2.0. To customize which class to instantiate for your post, use Class Maps instead: https://timber.github.io/docs/v2/guides/class-maps/',
1✔
392
                '2.0.0'
1✔
393
            );
1✔
394
        }
395
    }
396

397
    /**
398
     * Gets a collection of posts.
399
     *
400
     * Refer to the official documentation for
401
     * [WP_Query](https://developer.wordpress.org/reference/classes/wp_query/) for a list of all
402
     * the arguments that can be used for the `$query` parameter.
403
     *
404
     * @api
405
     * @example
406
     * ```php
407
     * // Use the global query.
408
     * $posts = Timber::get_posts();
409
     *
410
     * // Using the WP_Query argument format.
411
     * $posts = Timber::get_posts( [
412
     *    'post_type'     => 'article',
413
     *    'category_name' => 'sports',
414
     * ] );
415
     *
416
     * // Using a WP_Query instance.
417
     * $posts = Timber::get_posts( new WP_Query( [ 'post_type' => 'any' ) );
418
     *
419
     * // Using an array of post IDs.
420
     * $posts = Timber::get_posts( [ 47, 543, 3220 ] );
421
     * ```
422
     *
423
     * @param mixed $query  Optional. Query args. Default `false`, which means that Timber will use
424
     *                      the global query. Accepts an array of `WP_Query` arguments, a `WP_Query`
425
     *                      instance or a list of post IDs.
426
     * @param array $options {
427
     *     Optional. Options for the query.
428
     *
429
     *     @type bool $merge_default    Merge query parameters with the default query parameters of
430
     *                                  the current template. Default false.
431
     * }
432
     *
433
     * @return PostCollectionInterface|null Null if no query could be run with the used
434
     *                                              query parameters.
435
     */
436
    public static function get_posts(mixed $query = false, $options = [])
71✔
437
    {
438
        if (\is_string($query)) {
71✔
439
            Helper::doing_it_wrong(
2✔
440
                'Timber::get_posts()',
2✔
441
                "Querying posts by using a query string was removed in Timber 2.0. Pass in the query string as an options array instead. For example, change Timber::get_posts( 'post_type=portfolio&posts_per_page=3') to Timber::get_posts( [ 'post_type' => 'portfolio', 'posts_per_page' => 3 ] ). Learn more: https://timber.github.io/docs/v2/reference/timber-timber/#get_posts",
2✔
442
                '2.0.0'
2✔
443
            );
2✔
444

445
            $query = new WP_Query($query);
2✔
446
        }
447

448
        if (\is_string($options)) {
71✔
449
            Helper::doing_it_wrong(
2✔
450
                'Timber::get_posts()',
2✔
451
                'The $PostClass parameter for passing in the post class to use in Timber::get_posts() was replaced with an $options array in Timber 2.0. To customize which class to instantiate for your post, use Class Maps instead: https://timber.github.io/docs/v2/guides/class-maps/',
2✔
452
                '2.0.0'
2✔
453
            );
2✔
454
            $options = [];
2✔
455
        }
456

457
        if (3 === \func_num_args()) {
71✔
458
            Helper::doing_it_wrong(
1✔
459
                'Timber::get_posts()',
1✔
460
                'The $return_collection parameter to control whether a post collection is returned in Timber::get_posts() was removed in Timber 2.0.',
1✔
461
                '2.0.0'
1✔
462
            );
1✔
463
        }
464

465
        if (\is_array($query) && isset($query['numberposts'])) {
71✔
466
            Helper::doing_it_wrong(
1✔
467
                'Timber::get_posts()',
1✔
468
                'Using `numberposts` only works when using `get_posts()`, but not for Timber::get_posts(). Use `posts_per_page` instead.',
1✔
469
                '2.0.0'
1✔
470
            );
1✔
471
        }
472

473
        /**
474
         * @todo Are there any more default options to support?
475
         */
476
        $options = \wp_parse_args($options, [
71✔
477
            'merge_default' => false,
71✔
478
        ]);
71✔
479

480
        global $wp_query;
481

482
        if (\is_array($query) && $options['merge_default']) {
71✔
483
            $query = \wp_parse_args($query, $wp_query->query_vars);
1✔
484
        }
485

486
        $factory = new PostFactory();
71✔
487

488
        // Default to the global query.
489
        return $factory->from($query ?: $wp_query);
71✔
490
    }
491

492
    /**
493
     * Gets a post by title or slug.
494
     *
495
     * @api
496
     * @since 2.0.0
497
     * @example
498
     * ```php
499
     * // By slug
500
     * $post = Timber::get_post_by( 'slug', 'about-us' );
501
     *
502
     * // By title
503
     * $post = Timber::get_post_by( 'title', 'About us' );
504
     * ```
505
     *
506
     * @param string       $type         The type to look for. One of `slug` or `title`.
507
     * @param string       $search_value The post slug or post title to search for. When searching
508
     *                                   for `title`, this parameter doesn’t need to be
509
     *                                   case-sensitive, because the `=` comparison is used in
510
     *                                   MySQL.
511
     * @param array        $args {
512
     *     Optional. An array of arguments to configure what is returned.
513
     *
514
     *            @type string|array     $post_type   Optional. What WordPress post type to limit the
515
     *                                         results to. Defaults to 'any'
516
     *     @type string           $order_by    Optional. The field to sort by. Defaults to
517
     *                                         'post_date'
518
     *     @type string           $order       Optional. The sort to apply. Defaults to ASC
519
     *
520
     * }
521
     *
522
     * @return Post|null A Timber post or `null` if no post could be found. If multiple
523
     *                           posts with the same slug or title were found, it will select the
524
     *                           post with the oldest date.
525
     */
526
    public static function get_post_by($type, $search_value, $args = [])
8✔
527
    {
528
        $post_id = false;
8✔
529
        $args = \wp_parse_args($args, [
8✔
530
            'post_type' => 'any',
8✔
531
            'order_by' => 'post_date',
8✔
532
            'order' => 'ASC',
8✔
533
        ]);
8✔
534
        if ('slug' === $type) {
8✔
535
            $args = \wp_parse_args($args, [
5✔
536
                'name' => $search_value,
5✔
537
                'fields' => 'ids',
5✔
538
            ]);
5✔
539
            $query = new WP_Query($args);
5✔
540

541
            if ($query->post_count < 1) {
5✔
542
                return null;
1✔
543
            }
544

545
            $posts = $query->get_posts();
4✔
546
            $post_id = \array_shift($posts);
4✔
547
        } elseif ('title' === $type) {
3✔
548
            /**
549
             * The following section is inspired by post_exists() as well as get_page_by_title().
550
             *
551
             * These two functions always return the post with lowest ID. However, we want the post
552
             * with oldest post date.
553
             *
554
             * @see \post_exists()
555
             * @see \get_page_by_title()
556
             */
557
            global $wpdb;
558

559
            $sql = "SELECT ID FROM $wpdb->posts WHERE post_title = %s";
3✔
560
            $query_args = [$search_value];
3✔
561
            if (\is_array($args['post_type'])) {
3✔
562
                $post_type = \esc_sql($args['post_type']);
1✔
563
                $post_type_in_string = "'" . \implode("','", $args['post_type']) . "'";
1✔
564

565
                $sql .= " AND post_type IN ($post_type_in_string)";
1✔
566
            } elseif ('any' !== $args['post_type']) {
3✔
567
                $sql .= ' AND post_type = %s';
1✔
568
                $query_args[] = $args['post_type'];
1✔
569
            }
570

571
            // Always return the oldest post first.
572
            $sql .= ' ORDER BY post_date ASC';
3✔
573

574
            $post_id = $wpdb->get_var($wpdb->prepare($sql, $query_args));
3✔
575
        }
576

577
        if (!$post_id) {
7✔
578
            return null;
1✔
579
        }
580

581
        return self::get_post($post_id);
6✔
582
    }
583

584
    /**
585
     * Query post.
586
     *
587
     * @api
588
     * @deprecated since 2.0.0 Use `Timber::get_post()` instead.
589
     *
590
     *
591
     * @return Post|array|bool|null
592
     */
593
    public static function query_post(mixed $query = false, array $options = [])
2✔
594
    {
595
        Helper::deprecated('Timber::query_post()', 'Timber::get_post()', '2.0.0');
2✔
596

597
        return self::get_post($query, $options);
2✔
598
    }
599

600
    /**
601
     * Query posts.
602
     *
603
     * @api
604
     * @deprecated since 2.0.0 Use `Timber::get_posts()` instead.
605
     *
606
     *
607
     * @return PostCollectionInterface
608
     */
609
    public static function query_posts(mixed $query = false, array $options = [])
1✔
610
    {
611
        Helper::deprecated('Timber::query_posts()', 'Timber::get_posts()', '2.0.0');
1✔
612

613
        return self::get_posts($query, $options);
1✔
614
    }
615

616
    /**
617
     * Gets an attachment by its URL or absolute file path.
618
     *
619
     * Honors the `timber/post/image_extensions` filter, returning a Timber\Image if the found
620
     * attachment is identified as an image. Also honors Class Maps.
621
     *
622
     * @api
623
     * @since 2.0.0
624
     * @example
625
     * ```php
626
     * // Get attachment by URL.
627
     * $attachment = Timber::get_attachment_by( 'url', 'https://example.com/uploads/2020/09/cat.gif' );
628
     *
629
     * // Get attachment by filepath.
630
     * $attachment = Timber::get_attachment_by( 'path', '/path/to/wp-content/uploads/2020/09/cat.gif' );
631
     *
632
     * // Try to handle either case.
633
     * $mystery_string = some_function();
634
     * $attachment     = Timber::get_attachment_by( $mystery_string );
635
     * ```
636
     *
637
     * @param string $field_or_ident Can be "url", "path", an attachment URL, or the absolute
638
     *                               path of an attachment file. If "url" or "path" is given, a
639
     *                               second arg is required.
640
     * @param string $ident          Optional. An attachment URL or absolute path. Default empty
641
     *                               string.
642
     *
643
     * @return Attachment|null
644
     */
645
    public static function get_attachment_by(string $field_or_ident, string $ident = '')
10✔
646
    {
647
        if ($field_or_ident === 'url') {
10✔
648
            if (empty($ident)) {
9✔
649
                Helper::doing_it_wrong(
1✔
650
                    'Timber::get_attachment_by()',
1✔
651
                    'Passing "url" as the first arg requires passing a URL as the second arg.',
1✔
652
                    '2.0.0'
1✔
653
                );
1✔
654

655
                return null;
1✔
656
            }
657

658
            $id = \attachment_url_to_postid($ident);
8✔
659

660
            return $id ? (new PostFactory())->from($id) : null;
8✔
661
        }
662

663
        if ($field_or_ident === 'path') {
7✔
664
            if (empty($ident)) {
6✔
665
                Helper::doing_it_wrong(
1✔
666
                    'Timber::get_attachment_by()',
1✔
667
                    'Passing "path" as the first arg requires passing an absolute path as the second arg.',
1✔
668
                    '2.0.0'
1✔
669
                );
1✔
670

671
                return null;
1✔
672
            }
673

674
            if (!\file_exists($ident)) {
5✔
675
                // Deal with a relative path.
676
                $ident = URLHelper::get_full_path($ident);
2✔
677
            }
678

679
            return self::get_attachment_by('url', URLHelper::file_system_to_url($ident));
5✔
680
        }
681

682
        if (empty($ident)) {
3✔
683
            $field = URLHelper::starts_with($field_or_ident, ABSPATH) ? 'path' : 'url';
3✔
684

685
            return self::get_attachment_by($field, $field_or_ident);
3✔
686
        }
687

688
        return null;
×
689
    }
690

691
    /* Term Retrieval
692
    ================================ */
693

694
    /**
695
     * Gets terms.
696
     *
697
     * @api
698
     * @see https://developer.wordpress.org/reference/classes/wp_term_query/__construct/
699
     * @example
700
     * ```php
701
     * // Get all tags.
702
     * $tags = Timber::get_terms( 'post_tag' );
703
     * // Note that this is equivalent to:
704
     * $tags = Timber::get_terms( 'tag' );
705
     * $tags = Timber::get_terms( 'tags' );
706
     *
707
     * // Get all categories.
708
     * $cats = Timber::get_terms( 'category' );
709
     *
710
     * // Get all terms in a custom taxonomy.
711
     * $cats = Timber::get_terms('my_taxonomy');
712
     *
713
     * // Perform a custom Term query.
714
     * $cats = Timber::get_terms( [
715
     *   'taxonomy' => 'my_taxonomy',
716
     *   'orderby'  => 'slug',
717
     *   'order'    => 'DESC',
718
     * ] );
719
     * ```
720
     *
721
     * @param string|array $args    A string or array identifying the taxonomy or
722
     *                              `WP_Term_Query` args. Numeric strings are treated as term IDs;
723
     *                              non-numeric strings are treated as taxonomy names. Numeric
724
     *                              arrays are treated as a list a of term identifiers; associative
725
     *                              arrays are treated as args for `WP_Term_Query::__construct()`
726
     *                              and accept any valid parameters to that constructor.
727
     *                              Default `null`, which will get terms from all queryable
728
     *                              taxonomies.
729
     * @param array        $options Optional. None are currently supported. Default empty array.
730
     *
731
     * @return iterable
732
     */
733
    public static function get_terms($args = null, array $options = []): iterable
22✔
734
    {
735
        // default to all queryable taxonomies
736
        $args ??= [
22✔
737
            'taxonomy' => \get_taxonomies(),
22✔
738
        ];
22✔
739

740
        $factory = new TermFactory();
22✔
741

742
        return $factory->from($args);
22✔
743
    }
744

745
    /**
746
     * Gets a term.
747
     *
748
     * @api
749
     * @param int|WP_Term $term A WP_Term or term_id
750
     * @return Term|null
751
     * @example
752
     * ```php
753
     * // Get a Term.
754
     * $tag = Timber::get_term( 123 );
755
     * ```
756
     */
757
    public static function get_term($term = null)
68✔
758
    {
759
        if (null === $term) {
68✔
760
            // get the fallback term_id from the current query
761
            global $wp_query;
762
            $term = $wp_query->queried_object->term_id ?? null;
5✔
763
        }
764

765
        if (null === $term) {
68✔
766
            // not able to get term_id from the current query; bail
767
            return null;
×
768
        }
769

770
        $factory = new TermFactory();
68✔
771
        $terms = $factory->from($term);
68✔
772

773
        if (\is_array($terms)) {
68✔
774
            $terms = $terms[0];
3✔
775
        }
776

777
        return $terms;
68✔
778
    }
779

780
    /**
781
     * Gets a term by field.
782
     *
783
     * This function works like
784
     * [`get_term_by()`](https://developer.wordpress.org/reference/functions/get_term_by/), but
785
     * returns a `Timber\Term` object.
786
     *
787
     * @api
788
     * @since 2.0.0
789
     * @example
790
     * ```php
791
     * // Get a term by slug.
792
     * $term = Timber::get_term_by( 'slug', 'security' );
793
     *
794
     * // Get a term by name.
795
     * $term = Timber::get_term_by( 'name', 'Security' );
796
     *
797
     * // Get a term by slug from a specific taxonomy.
798
     * $term = Timber::get_term_by( 'slug', 'security', 'category' );
799
     * ```
800
     *
801
     * @param string     $field    The name of the field to retrieve the term with. One of: `id`,
802
     *                             `ID`, `slug`, `name` or `term_taxonomy_id`.
803
     * @param int|string $value    The value to search for by `$field`.
804
     * @param string     $taxonomy The taxonomy you want to retrieve from. Empty string will search
805
     *                             from all.
806
     *
807
     * @return Term|null
808
     */
809
    public static function get_term_by(string $field, $value, string $taxonomy = '')
3✔
810
    {
811
        $wp_term = \get_term_by($field, $value, $taxonomy);
3✔
812

813
        if ($wp_term === false) {
3✔
814
            if (empty($taxonomy) && $field != 'term_taxonomy_id') {
2✔
815
                $search = [
2✔
816
                    $field => $value,
2✔
817
                    $taxonomy => 'any',
2✔
818
                    'hide_empty' => false,
2✔
819
                ];
2✔
820
                return static::get_term($search);
2✔
821
            }
822

823
            return null;
×
824
        }
825

826
        return static::get_term($wp_term);
2✔
827
    }
828

829
    /* User Retrieval
830
    ================================ */
831

832
    /**
833
     * Gets one or more users as an array.
834
     *
835
     * By default, Timber will use the `Timber\User` class to create a your post objects. To
836
     * control which class is used for your post objects, use [Class Maps]().
837
     *
838
     * @api
839
     * @since 2.0.0
840
     * @example
841
     * ```php
842
     * // Get users with on an array of user IDs.
843
     * $users = Timber::get_users( [ 24, 81, 325 ] );
844
     *
845
     * // Get all users that only have a subscriber role.
846
     * $subscribers = Timber::get_users( [
847
     *     'role' => 'subscriber',
848
     * ] );
849
     *
850
     * // Get all users that have published posts.
851
     * $post_authors = Timber::get_users( [
852
     *     'has_published_posts' => [ 'post' ],
853
     * ] );
854
     * ```
855
     *
856
     * @todo  Add links to Class Maps documentation in function summary.
857
     *
858
     * @param array $query   Optional. A WordPress-style query or an array of user IDs. Use an
859
     *                       array in the same way you would use the `$args` parameter in
860
     *                       [WP_User_Query](https://developer.wordpress.org/reference/classes/wp_user_query/).
861
     *                       See
862
     *                       [WP_User_Query::prepare_query()](https://developer.wordpress.org/reference/classes/WP_User_Query/prepare_query/)
863
     *                       for a list of all available parameters. Passing an empty parameter
864
     *                       will return an empty array. Default empty array
865
     *                       `[]`.
866
     * @param array $options Optional. An array of options. None are currently supported. This
867
     *                       parameter exists to prevent future breaking changes. Default empty
868
     *                       array `[]`.
869
     *
870
     * @return iterable An array of users objects. Will be empty if no users were found.
871
     */
872
    public static function get_users(array $query = [], array $options = []): iterable
2✔
873
    {
874
        $factory = new UserFactory();
2✔
875

876
        return $factory->from($query);
2✔
877
    }
878

879
    /**
880
     * Gets a single user.
881
     *
882
     * By default, Timber will use the `Timber\User` class to create a your post objects. To
883
     * control which class is used for your post objects, use [Class Maps]().
884
     *
885
     * @api
886
     * @since 2.0.0
887
     * @example
888
     * ```php
889
     * $current_user = Timber::get_user();
890
     *
891
     * // Get user by ID.
892
     * $user = Timber::get_user( $user_id );
893
     *
894
     * // Convert a WP_User object to a Timber\User object.
895
     * $user = Timber::get_user( $wp_user_object );
896
     *
897
     * // Check if a user is logged in.
898
     *
899
     * $user = Timber::get_user();
900
     *
901
     * if ( $user ) {
902
     *     // Yay, user is logged in.
903
     * }
904
     * ```
905
     *
906
     * @todo Add links to Class Maps documentation in function summary.
907
     *
908
     * @param int|WP_User $user A WP_User object or a WordPress user ID. Defaults to the ID of the
909
     *                           currently logged-in user.
910
     *
911
     * @return User|null
912
     */
913
    public static function get_user($user = null)
59✔
914
    {
915
        /*
916
         * TODO in the interest of time, I'm implementing this logic here. If there's
917
         * a better place to do this or something that already implements this, let me know
918
         * and I'll switch over to that.
919
         */
920
        $user = $user ?: \get_current_user_id();
59✔
921

922
        $factory = new UserFactory();
59✔
923
        return $factory->from($user);
59✔
924
    }
925

926
    /**
927
     * Gets a user by field.
928
     *
929
     * This function works like
930
     * [`get_user_by()`](https://developer.wordpress.org/reference/functions/get_user_by/), but
931
     * returns a `Timber\User` object.
932
     *
933
     * @api
934
     * @since 2.0.0
935
     * @example
936
     * ```php
937
     * // Get a user by email.
938
     * $user = Timber::get_user_by( 'email', 'user@example.com' );
939
     *
940
     * // Get a user by login.
941
     * $user = Timber::get_user_by( 'login', 'keanu-reeves' );
942
     * ```
943
     *
944
     * @param string     $field The name of the field to retrieve the user with. One of: `id`,
945
     *                          `ID`, `slug`, `email` or `login`.
946
     * @param int|string $value The value to search for by `$field`.
947
     *
948
     * @return User|null
949
     */
950
    public static function get_user_by(string $field, $value)
4✔
951
    {
952
        $wp_user = \get_user_by($field, $value);
4✔
953

954
        if ($wp_user === false) {
4✔
955
            return null;
1✔
956
        }
957

958
        return static::get_user($wp_user);
3✔
959
    }
960

961
    /* Menu Retrieval
962
    ================================ */
963

964
    /**
965
     * Gets a nav menu object.
966
     *
967
     * @api
968
     * @since 2.0.0
969
     * @example
970
     * ```php
971
     * // Get a menu by location
972
     * $menu = Timber::get_menu( 'primary-menu' );
973
     *
974
     * // Get a menu by slug
975
     * $menu = Timber::get_menu( 'my-menu' );
976
     *
977
     * // Get a menu by name
978
     * $menu = Timber::get_menu( 'Main Menu' );
979
     *
980
     * // Get a menu by ID (term_id)
981
     * $menu = Timber::get_menu( 123 );
982
     * ```
983
     *
984
     * @param int|string $identifier A menu identifier: a term_id, slug, menu name, or menu location name
985
     * @param array      $args An associative array of options. Currently only one option is
986
     * supported:
987
     * - `depth`: How deep down the tree of menu items to query. Useful if you only want
988
     *   the first N levels of items in the menu.
989
     *
990
     * @return Menu|null
991
     */
992
    public static function get_menu($identifier = null, array $args = []): ?Menu
55✔
993
    {
994
        $factory = new MenuFactory();
55✔
995
        return $factory->from($identifier, $args);
55✔
996
    }
997

998
    /**
999
     * Gets a menu by field.
1000
     *
1001
     * @api
1002
     * @since 2.0.0
1003
     * @example
1004
     * ```php
1005
     * // Get a menu by location.
1006
     * $menu = Timber::get_menu_by( 'location', 'primary' );
1007
     *
1008
     * // Get a menu by slug.
1009
     * $menu = Timber::get_menu_by( 'slug', 'primary-menu' );
1010
     * ```
1011
     *
1012
     * @param string     $field The name of the field to retrieve the menu with. One of: `id`,
1013
     *                          `ID`, `term_id`, `slug`, `name` or `location`.
1014
     * @param int|string $value The value to search for by `$field`.
1015
     *
1016
     * @return Menu|null
1017
     */
1018
    public static function get_menu_by(string $field, $value, array $args = []): ?Menu
×
1019
    {
1020
        $factory = new MenuFactory();
×
1021
        $menu = null;
×
1022

1023
        $menu = match ($field) {
×
1024
            'id', 'term_id', 'ID' => $factory->from_id($value, $args),
×
1025
            'slug' => $factory->from_slug($value, $args),
×
1026
            'name' => $factory->from_name($value, $args),
×
1027
            'location' => $factory->from_location($value, $args),
×
1028
            default => $menu,
×
1029
        };
×
1030

1031
        return $menu;
×
1032
    }
1033

1034
    /**
1035
     * Gets a menu from the existing pages.
1036
     *
1037
     * @api
1038
     * @since 2.0.0
1039
     *
1040
     * @example
1041
     * ```php
1042
     * $menu = Timber::get_pages_menu();
1043
     * ```
1044
     *
1045
     * @param array $args Optional. Arguments for `wp_list_pages()`. Timber doesn’t use that
1046
     *                    function under the hood, but supports all arguments for that function.
1047
     *                    It will use `get_pages()` to get the pages that will be used for the Pages
1048
     *                    Menu.
1049
     */
1050
    public static function get_pages_menu(array $args = [])
9✔
1051
    {
1052
        $factory = new PagesMenuFactory();
9✔
1053

1054
        $menu = $factory->from_pages($args);
9✔
1055

1056
        return $menu;
9✔
1057
    }
1058

1059
    /**
1060
     * Get the navigation menu location assigned to the given menu.
1061
     *
1062
     * @api
1063
     * @since 2.3.0
1064
     *
1065
     * @param  WP_Term|int $term The menu to find; either a WP_Term object or a Term ID.
1066
     * @return string|null
1067
     */
1068
    public static function get_menu_location($term): ?string
65✔
1069
    {
1070
        if ($term instanceof WP_Term) {
65✔
1071
            $term_id = $term->term_id;
65✔
1072
        } elseif (\is_int($term)) {
1✔
1073
            $term_id = $term;
1✔
1074
        } else {
1075
            return null;
×
1076
        }
1077

1078
        $locations = \array_flip(static::get_menu_locations());
65✔
1079
        return $locations[$term_id] ?? null;
65✔
1080
    }
1081

1082
    /**
1083
     * Get the navigation menu locations with assigned menus.
1084
     *
1085
     * @api
1086
     * @since 2.3.0
1087
     *
1088
     * @return array<string, (int|string)>
1089
     */
1090
    public static function get_menu_locations(): array
65✔
1091
    {
1092
        return \array_filter(
65✔
1093
            \get_nav_menu_locations(),
65✔
1094
            fn ($location) => \is_string($location) || \is_int($location)
65✔
1095
        );
65✔
1096
    }
1097

1098
    /* Comment Retrieval
1099
    ================================ */
1100

1101
    /**
1102
     * Get comments.
1103
     *
1104
     * @api
1105
     * @since 2.0.0
1106
     *
1107
     * @param array|WP_Comment_Query $query
1108
     * @param array                   $options Optional. None are currently supported.
1109
     * @return array
1110
     */
1111
    public static function get_comments($query = [], array $options = []): iterable
×
1112
    {
1113
        $factory = new CommentFactory();
×
1114

1115
        return $factory->from($query);
×
1116
    }
1117

1118
    /**
1119
     * Gets comment.
1120
     *
1121
     * @api
1122
     * @since 2.0.0
1123
     * @param int|WP_Comment $comment
1124
     * @return Comment|null
1125
     */
1126
    public static function get_comment($comment)
43✔
1127
    {
1128
        $factory = new CommentFactory();
43✔
1129
        return $factory->from($comment);
43✔
1130
    }
1131

1132
    /* Site Retrieval
1133
    ================================ */
1134

1135
    /**
1136
     * Get sites.
1137
     * @api
1138
     * @param array|bool $blog_ids
1139
     * @return array
1140
     */
1141
    public static function get_sites($blog_ids = false)
4✔
1142
    {
1143
        if (!\is_array($blog_ids)) {
4✔
1144
            global $wpdb;
1145
            $blog_ids = $wpdb->get_col("SELECT blog_id FROM $wpdb->blogs ORDER BY blog_id ASC");
4✔
1146
        }
1147
        $return = [];
4✔
1148
        foreach ($blog_ids as $blog_id) {
4✔
1149
            $return[] = new Site($blog_id);
4✔
1150
        }
1151
        return $return;
4✔
1152
    }
1153

1154
    /*  Template Setup and Display
1155
    ================================ */
1156

1157
    /**
1158
     * Get context.
1159
     * @api
1160
     * @deprecated 2.0.0, use `Timber::context()` instead.
1161
     *
1162
     * @return array
1163
     */
1164
    public static function get_context()
×
1165
    {
1166
        Helper::deprecated('get_context', 'context', '2.0.0');
×
1167

1168
        return self::context();
×
1169
    }
1170

1171
    /**
1172
     * Gets the global context.
1173
     *
1174
     * The context always contains the global context with the following variables:
1175
     *
1176
     * - `site` – An instance of `Timber\Site`.
1177
     * - `request` - An instance of `Timber\Request`.
1178
     * - `theme` - An instance of `Timber\Theme`.
1179
     * - `user` - An instance of `Timber\User`.
1180
     * - `http_host` - The HTTP host.
1181
     * - `wp_title` - Title retrieved for the currently displayed page, retrieved through
1182
     * `wp_title()`.
1183
     * - `body_class` - The body class retrieved through `get_body_class()`.
1184
     *
1185
     * The global context will be cached, which means that you can call this function again without
1186
     * losing performance.
1187
     *
1188
     * In addition to that, the context will contain template contexts depending on which template
1189
     * is being displayed. For archive templates, a `posts` variable will be present that will
1190
     * contain a collection of `Timber\Post` objects for the default query. For singular templates,
1191
     * a `post` variable will be present that that contains a `Timber\Post` object of the `$post`
1192
     * global.
1193
     *
1194
     * @api
1195
     * @since 2.0.0
1196
     *
1197
     * @param array $extra Any extra data to merge in. Overrides whatever is already there for this
1198
     *                     call only. In other words, the underlying context data is immutable and
1199
     *                     unaffected by passing this param.
1200
     *
1201
     * @return array An array of context variables that is used to pass into Twig templates through
1202
     *               a render or compile function.
1203
     */
1204
    public static function context(array $extra = [])
43✔
1205
    {
1206
        $context = self::context_global();
43✔
1207

1208
        if (\is_singular()) {
43✔
1209
            // NOTE: this also handles the is_front_page() case.
1210
            $context['post'] = Timber::get_post()?->setup();
6✔
1211
        } elseif (\is_home()) {
37✔
1212
            $context['post'] = Timber::get_post()?->setup();
4✔
1213
            $context['posts'] = Timber::get_posts();
4✔
1214
        } elseif (\is_category() || \is_tag() || \is_tax()) {
33✔
1215
            $context['term'] = Timber::get_term();
4✔
1216
            $context['posts'] = Timber::get_posts();
4✔
1217
        } elseif (\is_search()) {
29✔
1218
            $context['posts'] = Timber::get_posts();
1✔
1219
            $context['search_query'] = \get_search_query();
1✔
1220
        } elseif (\is_author()) {
28✔
1221
            $context['author'] = Timber::get_user(\get_query_var('author'));
1✔
1222
            $context['posts'] = Timber::get_posts();
1✔
1223
        } elseif (\is_archive()) {
27✔
1224
            $context['posts'] = Timber::get_posts();
×
1225
        }
1226

1227
        return \array_merge($context, $extra);
43✔
1228
    }
1229

1230
    /**
1231
     * Gets the global context.
1232
     *
1233
     * This function is used by `Timber::context()` to get the global context. Usually, you don’t
1234
     * call this function directly, except when you need the global context in a partial view.
1235
     *
1236
     * The global context will be cached, which means that you can call this function again without
1237
     * losing performance.
1238
     *
1239
     * @api
1240
     * @since 2.0.0
1241
     * @example
1242
     * ```php
1243
     * add_shortcode( 'global_address', function() {
1244
     *     return Timber::compile(
1245
     *         'global_address.twig',
1246
     *         Timber::context_global()
1247
     *     );
1248
     * } );
1249
     * ```
1250
     *
1251
     * @return array An array of global context variables.
1252
     */
1253
    public static function context_global()
43✔
1254
    {
1255
        if (empty(self::$context_cache)) {
43✔
1256
            self::$context_cache['site'] = new Site();
43✔
1257
            self::$context_cache['theme'] = self::$context_cache['site']->theme;
43✔
1258
            self::$context_cache['user'] = \is_user_logged_in() ? static::get_user() : false;
43✔
1259

1260
            self::$context_cache['http_host'] = URLHelper::get_scheme() . '://' . URLHelper::get_host();
43✔
1261
            self::$context_cache['wp_title'] = Helper::get_wp_title();
43✔
1262
            self::$context_cache['body_class'] = \implode(' ', \get_body_class());
43✔
1263

1264
            /**
1265
             * Filters the global Timber context.
1266
             *
1267
             * By using this filter, you can add custom data to the global Timber context, which
1268
             * means that this data will be available on every page that is initialized with
1269
             * `Timber::context()`.
1270
             *
1271
             * Be aware that data will be cached as soon as you call `Timber::context()` for the
1272
             * first time. That’s why you should add this filter before you call
1273
             * `Timber::context()`.
1274
             *
1275
             * @see \Timber\Timber::context()
1276
             * @since 0.21.7
1277
             * @example
1278
             * ```php
1279
             * add_filter( 'timber/context', function( $context ) {
1280
             *     // Example: A custom value
1281
             *     $context['custom_site_value'] = 'Hooray!';
1282
             *
1283
             *     // Example: Add a menu to the global context.
1284
             *     $context['menu'] = new \Timber\Menu( 'primary-menu' );
1285
             *
1286
             *     // Example: Add all ACF options to global context.
1287
             *     $context['options'] = get_fields( 'options' );
1288
             *
1289
             *     return $context;
1290
             * } );
1291
             * ```
1292
             * ```twig
1293
             * <h1>{{ custom_site_value|e }}</h1>
1294
             *
1295
             * {% for item in menu.items %}
1296
             *     {# Display menu item #}
1297
             * {% endfor %}
1298
             *
1299
             * <footer>
1300
             *     {% if options.footer_text is not empty %}
1301
             *         {{ options.footer_text|e }}
1302
             *     {% endif %}
1303
             * </footer>
1304
             * ```
1305
             *
1306
             * @param array $context The global context.
1307
             */
1308
            self::$context_cache = \apply_filters('timber/context', self::$context_cache);
43✔
1309

1310
            /**
1311
             * Filters the global Timber context.
1312
             *
1313
             * @deprecated 2.0.0, use `timber/context`
1314
             */
1315
            self::$context_cache = \apply_filters_deprecated(
43✔
1316
                'timber_context',
43✔
1317
                [self::$context_cache],
43✔
1318
                '2.0.0',
43✔
1319
                'timber/context'
43✔
1320
            );
43✔
1321
        }
1322

1323
        return self::$context_cache;
43✔
1324
    }
1325

1326
    /**
1327
     * Compiles a Twig file.
1328
     *
1329
     * Passes data to a Twig file and returns the output. If the template file doesn't exist it will throw a warning
1330
     * when WP_DEBUG is enabled.
1331
     *
1332
     * @api
1333
     * @example
1334
     * ```php
1335
     * $data = array(
1336
     *     'firstname' => 'Jane',
1337
     *     'lastname' => 'Doe',
1338
     *     'email' => 'jane.doe@example.org',
1339
     * );
1340
     *
1341
     * $team_member = Timber::compile( 'team-member.twig', $data );
1342
     * ```
1343
     * @param array|string    $filenames        Name or full path of the Twig file to compile. If this is an array of file
1344
     *                                          names or paths, Timber will compile the first file that exists.
1345
     * @param array           $data             Optional. An array of data to use in Twig template.
1346
     * @param bool|int|array  $expires          Optional. In seconds. Use false to disable cache altogether. When passed an
1347
     *                                          array, the first value is used for non-logged in visitors, the second for users.
1348
     *                                          Default false.
1349
     * @param string          $cache_mode       Optional. Any of the cache mode constants defined in Timber\Loader.
1350
     * @param bool            $via_render       Optional. Whether to apply optional render or compile filters. Default false.
1351
     * @return bool|string                      The returned output.
1352
     */
1353
    public static function compile($filenames, $data = [], $expires = false, $cache_mode = Loader::CACHE_USE_DEFAULT, $via_render = false)
85✔
1354
    {
1355
        if (!\defined('TIMBER_LOADED')) {
85✔
1356
            self::init();
×
1357
        }
1358
        $caller = LocationManager::get_calling_script_dir(1);
85✔
1359
        $loader = new Loader($caller);
85✔
1360
        $file = $loader->choose_template($filenames);
85✔
1361

1362
        $caller_file = LocationManager::get_calling_script_file(1);
85✔
1363

1364
        /**
1365
         * Fires after the calling PHP file was determined in Timber’s compile
1366
         * function.
1367
         *
1368
         * This action is used by the Timber Debug Bar extension.
1369
         *
1370
         * @since 1.1.2
1371
         * @since 2.0.0 Switched from filter to action.
1372
         *
1373
         * @param string|null $caller_file The calling script file.
1374
         */
1375
        \do_action('timber/calling_php_file', $caller_file);
85✔
1376

1377
        if ($via_render) {
85✔
1378
            /**
1379
             * Filters the Twig template that should be rendered.
1380
             *
1381
             * @since 2.0.0
1382
             *
1383
             * @param string $file The chosen Twig template name to render.
1384
             */
1385
            $file = \apply_filters('timber/render/file', $file);
3✔
1386

1387
            /**
1388
             * Filters the Twig file that should be rendered.
1389
             *
1390
             * @codeCoverageIgnore
1391
             * @deprecated 2.0.0, use `timber/render/file`
1392
             */
1393
            $file = \apply_filters_deprecated(
3✔
1394
                'timber_render_file',
3✔
1395
                [$file],
3✔
1396
                '2.0.0',
3✔
1397
                'timber/render/file'
3✔
1398
            );
3✔
1399
        } else {
1400
            /**
1401
             * Filters the Twig template that should be compiled.
1402
             *
1403
             * @since 2.0.0
1404
             *
1405
             * @param string $file The chosen Twig template name to compile.
1406
             */
1407
            $file = \apply_filters('timber/compile/file', $file);
82✔
1408

1409
            /**
1410
             * Filters the Twig template that should be compiled.
1411
             *
1412
             * @deprecated 2.0.0
1413
             */
1414
            $file = \apply_filters_deprecated(
82✔
1415
                'timber_compile_file',
82✔
1416
                [$file],
82✔
1417
                '2.0.0',
82✔
1418
                'timber/compile/file'
82✔
1419
            );
82✔
1420
        }
1421
        $output = false;
85✔
1422

1423
        if ($file !== false) {
85✔
1424
            if (\is_null($data)) {
81✔
1425
                $data = [];
2✔
1426
            }
1427

1428
            if ($via_render) {
81✔
1429
                /**
1430
                 * Filters the data that should be passed for rendering a Twig template.
1431
                 *
1432
                 * @since 2.0.0
1433
                 *
1434
                 * @param array  $data The data that is used to render the Twig template.
1435
                 * @param string $file The name of the Twig template to render.
1436
                 */
1437
                $data = \apply_filters('timber/render/data', $data, $file);
3✔
1438
                /**
1439
                 * Filters the data that should be passed for rendering a Twig template.
1440
                 *
1441
                 * @codeCoverageIgnore
1442
                 * @deprecated 2.0.0
1443
                 */
1444
                $data = \apply_filters_deprecated(
3✔
1445
                    'timber_render_data',
3✔
1446
                    [$data],
3✔
1447
                    '2.0.0',
3✔
1448
                    'timber/render/data'
3✔
1449
                );
3✔
1450
            } else {
1451
                /**
1452
                 * Filters the data that should be passed for compiling a Twig template.
1453
                 *
1454
                 * @since 2.0.0
1455
                 *
1456
                 * @param array  $data The data that is used to compile the Twig template.
1457
                 * @param string $file The name of the Twig template to compile.
1458
                 */
1459
                $data = \apply_filters('timber/compile/data', $data, $file);
78✔
1460

1461
                /**
1462
                 * Filters the data that should be passed for compiling a Twig template.
1463
                 *
1464
                 * @deprecated 2.0.0, use `timber/compile/data`
1465
                 */
1466
                $data = \apply_filters_deprecated(
78✔
1467
                    'timber_compile_data',
78✔
1468
                    [$data],
78✔
1469
                    '2.0.0',
78✔
1470
                    'timber/compile/data'
78✔
1471
                );
78✔
1472
            }
1473

1474
            $output = $loader->render($file, $data, $expires, $cache_mode);
81✔
1475
        } else {
1476
            if (\is_array($filenames)) {
4✔
1477
                $filenames = \implode(", ", $filenames);
1✔
1478
            }
1479
            Helper::error_log('Error loading your template files: ' . $filenames . '. Make sure one of these files exists.');
4✔
1480
        }
1481

1482
        /**
1483
         * Filters the compiled result before it is returned in `Timber::compile()`.
1484
         *
1485
         * It adds the possibility to filter the output ready for render.
1486
         *
1487
         * @since 2.0.0
1488
         *
1489
         * @param string|bool $output the compiled output.
1490
         */
1491
        $output = \apply_filters('timber/compile/result', $output);
84✔
1492

1493
        /**
1494
         * Fires after a Twig template was compiled and before the compiled data
1495
         * is returned.
1496
         *
1497
         * This action can be helpful if you need to debug Twig template
1498
         * compilation.
1499
         *
1500
         * @since 2.0.0
1501
         *
1502
         * @param string            $output       The compiled output.
1503
         * @param string            $file         The name of the Twig template that was compiled.
1504
         * @param array             $data         The data that was used to compile the Twig template.
1505
         * @param bool|int|array    $expires      The expiration time of the cache in seconds, or false to disable cache.
1506
         * @param string            $cache_mode   Any of the cache mode constants defined in Timber\Loader.
1507
         */
1508
        \do_action('timber/compile/done', $output, $file, $data, $expires, $cache_mode);
84✔
1509

1510
        /**
1511
         * Fires after a Twig template was compiled and before the compiled data
1512
         * is returned.
1513
         *
1514
         * @deprecated 2.0.0, use `timber/compile/done`
1515
         */
1516
        \do_action_deprecated('timber_compile_done', [], '2.0.0', 'timber/compile/done');
84✔
1517

1518
        return $output;
84✔
1519
    }
1520

1521
    /**
1522
     * Compile a string.
1523
     *
1524
     * This function compiles a string with Twig variables and returns the output.
1525
     * Please be aware that this function might cause a security risk if you pass untrusted data into this function.
1526
     * If you want to minimize the risk, you could remove fn from the Timber function array. See https://timber.github.io/docs/v2/hooks/filters/#timber/twig/functions
1527
     *
1528
     * @api
1529
     * @example
1530
     * ```php
1531
     * $data = array(
1532
     *     'username' => 'Jane Doe',
1533
     * );
1534
     *
1535
     * $welcome = Timber::compile_string( 'Hi {{ username }}, I’m a string with a custom Twig variable', $data );
1536
     * ```
1537
     * @param string $string A string with Twig variables.
1538
     * @param array  $data   Optional. An array of data to use in Twig template.
1539
     * @return bool|string
1540
     */
1541
    public static function compile_string($string, $data = [])
329✔
1542
    {
1543
        $dummy_loader = new Loader();
329✔
1544
        $twig = $dummy_loader->get_twig();
329✔
1545
        $template = $twig->createTemplate($string);
329✔
1546
        return $template->render($data);
327✔
1547
    }
1548

1549
    /**
1550
     * Fetch function.
1551
     *
1552
     * @api
1553
     * @deprecated 2.0.0 use Timber::compile()
1554
     * @param array|string $filenames  Name of the Twig file to render. If this is an array of files, Timber will
1555
     *                                 render the first file that exists.
1556
     * @param array        $data       Optional. An array of data to use in Twig template.
1557
     * @param bool|int     $expires    Optional. In seconds. Use false to disable cache altogether. When passed an
1558
     *                                 array, the first value is used for non-logged in visitors, the second for users.
1559
     *                                 Default false.
1560
     * @param string       $cache_mode Optional. Any of the cache mode constants defined in Timber\Loader.
1561
     * @return bool|string The returned output.
1562
     */
1563
    public static function fetch($filenames, $data = [], $expires = false, $cache_mode = Loader::CACHE_USE_DEFAULT)
×
1564
    {
1565
        Helper::deprecated(
×
1566
            'fetch',
×
1567
            'Timber::compile() (see https://timber.github.io/docs/v2/reference/timber/#compile for more information)',
×
1568
            '2.0.0'
×
1569
        );
×
1570
        $output = self::compile($filenames, $data, $expires, $cache_mode, true);
×
1571

1572
        /**
1573
         * Filters the compiled result before it is returned.
1574
         *
1575
         * @see \Timber\Timber::fetch()
1576
         * @since 0.16.7
1577
         * @deprecated 2.0.0 use timber/compile/result
1578
         *
1579
         * @param string $output The compiled output.
1580
         */
1581
        $output = \apply_filters_deprecated(
×
1582
            'timber_compile_result',
×
1583
            [$output],
×
1584
            '2.0.0',
×
1585
            'timber/compile/result'
×
1586
        );
×
1587

1588
        return $output;
×
1589
    }
1590

1591
    /**
1592
     * Renders a Twig file.
1593
     *
1594
     * Passes data to a Twig file and echoes the output.
1595
     *
1596
     * @api
1597
     * @example
1598
     * ```php
1599
     * $context = Timber::context();
1600
     *
1601
     * Timber::render( 'index.twig', $context );
1602
     * ```
1603
     * @param array|string   $filenames      Name or full path of the Twig file to render. If this is an array of file
1604
     *                                       names or paths, Timber will render the first file that exists.
1605
     * @param array          $data           Optional. An array of data to use in Twig template.
1606
     * @param bool|int|array $expires        Optional. In seconds. Use false to disable cache altogether. When passed an
1607
     *                                       array, the first value is used for non-logged in visitors, the second for users.
1608
     *                                       Default false.
1609
     * @param string         $cache_mode     Optional. Any of the cache mode constants defined in Timber\Loader.
1610
     */
1611
    public static function render($filenames, $data = [], $expires = false, $cache_mode = Loader::CACHE_USE_DEFAULT): void
3✔
1612
    {
1613
        $output = self::compile($filenames, $data, $expires, $cache_mode, true);
3✔
1614
        echo $output;
3✔
1615
    }
1616

1617
    /**
1618
     * Render a string with Twig variables.
1619
     *
1620
     * @api
1621
     * @example
1622
     * ```php
1623
     * $data = array(
1624
     *     'username' => 'Jane Doe',
1625
     * );
1626
     *
1627
     * Timber::render_string( 'Hi {{ username }}, I’m a string with a custom Twig variable', $data );
1628
     * ```
1629
     * @param string $string A string with Twig variables.
1630
     * @param array  $data   An array of data to use in Twig template.
1631
     */
1632
    public static function render_string($string, $data = [])
2✔
1633
    {
1634
        $compiled = self::compile_string($string, $data);
2✔
1635
        echo $compiled;
2✔
1636
    }
1637

1638
    /*  Sidebar
1639
    ================================ */
1640

1641
    /**
1642
     * Get sidebar.
1643
     * @api
1644
     * @param string  $sidebar
1645
     * @param array   $data
1646
     * @return bool|string
1647
     */
1648
    public static function get_sidebar($sidebar = 'sidebar.php', $data = [])
2✔
1649
    {
1650
        if (\strstr(\strtolower($sidebar), '.php')) {
2✔
1651
            return self::get_sidebar_from_php($sidebar, $data);
1✔
1652
        }
1653
        return self::compile($sidebar, $data);
1✔
1654
    }
1655

1656
    /**
1657
     * Get sidebar from PHP
1658
     * @api
1659
     * @param string  $sidebar
1660
     * @param array   $data
1661
     * @return string
1662
     */
1663
    public static function get_sidebar_from_php($sidebar = '', $data = [])
1✔
1664
    {
1665
        $caller = LocationManager::get_calling_script_dir(1);
1✔
1666
        $uris = LocationManager::get_locations($caller);
1✔
1667
        \ob_start();
1✔
1668
        $found = false;
1✔
1669
        foreach ($uris as $namespace => $uri_locations) {
1✔
1670
            if (\is_array($uri_locations)) {
1✔
1671
                foreach ($uri_locations as $uri) {
1✔
1672
                    if (\file_exists(\trailingslashit($uri) . $sidebar)) {
1✔
1673
                        include \trailingslashit($uri) . $sidebar;
1✔
1674
                        $found = true;
1✔
1675
                    }
1676
                }
1677
            }
1678
        }
1679
        if (!$found) {
1✔
1680
            Helper::error_log('error loading your sidebar, check to make sure the file exists');
×
1681
        }
1682
        $ret = \ob_get_contents();
1✔
1683
        \ob_end_clean();
1✔
1684

1685
        return $ret;
1✔
1686
    }
1687

1688
    /**
1689
     * Get widgets.
1690
     *
1691
     * @api
1692
     * @param int|string $widget_id Optional. Index, name or ID of dynamic sidebar. Default 1.
1693
     * @return string
1694
     */
1695
    public static function get_widgets($widget_id)
×
1696
    {
1697
        return \trim(Helper::ob_function('dynamic_sidebar', [$widget_id]));
×
1698
    }
1699

1700
    /**
1701
     * Get pagination.
1702
     *
1703
     * @api
1704
     * @deprecated 2.0.0
1705
     * @link https://timber.github.io/docs/v2/guides/pagination/
1706
     * @param array $prefs an array of preference data.
1707
     * @return array|mixed
1708
     */
1709
    public static function get_pagination($prefs = [])
16✔
1710
    {
1711
        Helper::deprecated(
16✔
1712
            'get_pagination',
16✔
1713
            '{{ posts.pagination }} (see https://timber.github.io/docs/v2/guides/pagination/ for more information)',
16✔
1714
            '2.0.0'
16✔
1715
        );
16✔
1716

1717
        return Pagination::get_pagination($prefs);
16✔
1718
    }
1719
}
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