• 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

79.79
/src/Helper.php
1
<?php
2

3
namespace Timber;
4

5
use Exception;
6
use InvalidArgumentException;
7
use stdClass;
8
use Timber\Factory\PostFactory;
9
use WP_List_Util;
10
use WP_Post;
11
use WP_Term;
12
use WP_User;
13

14
/**
15
 * Class Helper
16
 *
17
 * As the name suggests these are helpers for Timber (and you!) when developing. You can find additional
18
 * (mainly internally-focused helpers) in Timber\URLHelper.
19
 * @api
20
 */
21
class Helper
22
{
23
    /**
24
     * A utility for a one-stop shop for transients.
25
     *
26
     * @api
27
     * @example
28
     * ```php
29
     * $context = Timber::context( [
30
     *     'favorites' => Timber\Helper::transient( 'user-' . $uid . '-favorites' , function() use ( $uid ) {
31
     *          // Some expensive query here that’s doing something you want to store to a transient.
32
     *          return $favorites;
33
     *     }, 600 ),
34
     * ] );
35
     *
36
     * Timber::render('single.twig', $context);
37
     * ```
38
     *
39
     * @param string      $slug           Unique identifier for transient
40
     * @param callable     $callback      Callback that generates the data that's to be cached
41
     * @param integer      $transient_time (optional) Expiration of transients in seconds
42
     * @param integer     $lock_timeout   (optional) How long (in seconds) to lock the transient to prevent race conditions
43
     * @param boolean     $force          (optional) Force callback to be executed when transient is locked
44
     *
45
     * @return mixed
46
     */
47
    public static function transient($slug, $callback, $transient_time = 0, $lock_timeout = 5, $force = false)
11✔
48
    {
49
        /**
50
         * Filters the transient slug.
51
         *
52
         * This might be useful if you are using a multilingual solution.
53
         *
54
         * @since 0.22.6
55
         *
56
         * @param string $slug The slug for the transient.
57
         */
58
        $slug = \apply_filters('timber/transient/slug', $slug);
11✔
59

60
        $enable_transients = ($transient_time === false || (\defined('WP_DISABLE_TRANSIENTS') && WP_DISABLE_TRANSIENTS)) ? false : true;
11✔
61
        $data = $enable_transients ? \get_transient($slug) : false;
11✔
62

63
        if (false === $data) {
11✔
64
            $data = self::handle_transient_locking($slug, $callback, $transient_time, $lock_timeout, $force, $enable_transients);
11✔
65
        }
66
        return $data;
11✔
67
    }
68

69
    /**
70
     * Does the dirty work of locking the transient, running the callback and unlocking.
71
     *
72
     * @internal
73
     *
74
     * @param string      $slug              Unique identifier for transient
75
     * @param callable    $callback          Callback that generates the data that's to be cached
76
     * @param integer     $transient_time    Expiration of transients in seconds
77
     * @param integer     $lock_timeout      How long (in seconds) to lock the transient to prevent race conditions
78
     * @param boolean     $force             Force callback to be executed when transient is locked
79
     * @param boolean     $enable_transients Force callback to be executed when transient is locked
80
     */
81
    protected static function handle_transient_locking($slug, $callback, $transient_time, $lock_timeout, $force, $enable_transients)
11✔
82
    {
83
        if ($enable_transients && self::_is_transient_locked($slug)) {
11✔
84
            /**
85
             * Filters whether to force a locked transients to be regenerated.
86
             *
87
             * If a transient is locked, it means that another process is currently generating the data.
88
             * If you want to force the transient to be regenerated, during that process, you can set this
89
             * filter to true.
90
             *
91
             * @since 2.0.0
92
             * @param bool $force Whether to force a locked transient to be regenerated.
93
             */
94
            $force = \apply_filters('timber/transient/force_transients', $force);
5✔
95

96
            /**
97
             * Filters whether to force a locked transients to be regenerated.
98
             *
99
             * If a transient is locked, it means that another process is currently generating the data.
100
             * If you want to force the transient to be regenerated, during that process, you can set this
101
             * filter to true.
102
             *
103
             * @deprecated 2.0.0, use `timber/transient/force_transients`
104
             */
105
            $force = \apply_filters_deprecated(
5✔
106
                'timber_force_transients',
5✔
107
                [$force],
5✔
108
                '2.0.0',
5✔
109
                'timber/transient/force_transients'
5✔
110
            );
5✔
111

112
            /**
113
             * Filters whether to force a specific locked transients to be regenerated.
114
             *
115
             * If a transient is locked, it means that another process is currently generating the data.
116
             * If you want to force the transient to be regenerated during that process, you can set this value to true.
117
             *
118
             * @example
119
             * ```php
120
             *
121
             * add_filter( 'timber/transient/force_transient_mycustumslug', function($force) {
122
             *     if(false == something_special_has_occurred()){
123
             *       return false;
124
             *     }
125
             *
126
             *     return true;
127
             * }, 10 );
128
             * ```
129
             * @since 2.0.0
130
             *
131
             * @param bool $force Whether to force a locked transient to be regenerated.
132
             */
133
            $force = \apply_filters("timber/transient/force_transient_{$slug}", $force);
5✔
134

135
            /**
136
             * Filters whether to force a specific locked transients to be regenerated.
137
             *
138
             * If a transient is locked, it means that another process is currently generating the data.
139
             * If you want to force the transient to be regenerated, during that process, you can set this value to true.
140
             * `$slug` The transient slug.
141
             *
142
             * @param bool $force Whether to force a locked transient to be regenerated.
143
             * @deprecated 2.0.0, use `timber/transient/force_transient_{$slug}`
144
             */
145
            $force = \apply_filters_deprecated(
5✔
146
                "timber_force_transient_{$slug}",
5✔
147
                [$force],
5✔
148
                '2.0.0',
5✔
149
                "timber/transient/force_transient_{$slug}"
5✔
150
            );
5✔
151

152
            if (!$force) {
5✔
153
                //the server is currently executing the process.
154
                //We're just gonna dump these users. Sorry!
155
                return false;
2✔
156
            }
157
            $enable_transients = false;
3✔
158
        }
159
        // lock timeout shouldn't be higher than 5 seconds, unless
160
        // remote calls with high timeouts are made here
161
        if ($enable_transients) {
9✔
162
            self::_lock_transient($slug, $lock_timeout);
6✔
163
        }
164
        $data = $callback();
9✔
165
        if ($enable_transients) {
9✔
166
            \set_transient($slug, $data, $transient_time);
6✔
167
            self::_unlock_transient($slug);
6✔
168
        }
169
        return $data;
9✔
170
    }
171

172
    /**
173
     * @internal
174
     * @param string $slug
175
     * @param integer $lock_timeout
176
     */
177
    public static function _lock_transient($slug, $lock_timeout)
14✔
178
    {
179
        \set_transient($slug . '_lock', true, $lock_timeout);
14✔
180
    }
181

182
    /**
183
     * @internal
184
     * @param string $slug
185
     */
186
    public static function _unlock_transient($slug)
7✔
187
    {
188
        \delete_transient($slug . '_lock');
7✔
189
    }
190

191
    /**
192
     * @internal
193
     * @param string $slug
194
     */
195
    public static function _is_transient_locked($slug)
14✔
196
    {
197
        return (bool) \get_transient($slug . '_lock');
14✔
198
    }
199

200
    /* These are for measuring page render time */
201

202
    /**
203
     * For measuring time, this will start a timer.
204
     *
205
     * @api
206
     * @return float
207
     */
208
    public static function start_timer()
1✔
209
    {
210
        $time = \microtime();
1✔
211
        $time = \explode(' ', $time);
1✔
212
        $time = (float) $time[1] + (float) $time[0];
1✔
213
        return $time;
1✔
214
    }
215

216
    /**
217
     * For stopping time and getting the data.
218
     *
219
     * @api
220
     * @example
221
     * ```php
222
     * $start = Timber\Helper::start_timer();
223
     * // do some stuff that takes awhile
224
     * echo Timber\Helper::stop_timer( $start );
225
     * ```
226
     *
227
     * @param int     $start
228
     * @return string
229
     */
230
    public static function stop_timer($start)
1✔
231
    {
232
        $time = \microtime();
1✔
233
        $time = \explode(' ', $time);
1✔
234
        $time = (float) $time[1] + (float) $time[0];
1✔
235
        $finish = $time;
1✔
236
        $total_time = \round(($finish - $start), 4);
1✔
237
        return $total_time . ' seconds.';
1✔
238
    }
239

240
    /* Function Utilities
241
    ======================== */
242

243
    /**
244
     * Calls a function with an output buffer. This is useful if you have a function that outputs
245
     * text that you want to capture and use within a twig template.
246
     *
247
     * @api
248
     * @example
249
     * ```php
250
     * function the_form() {
251
     *     echo '<form action="form.php"><input type="text" /><input type="submit /></form>';
252
     * }
253
     *
254
     * $context = Timber::context( [
255
     *     'form' => Timber\Helper::ob_function( 'the_form' ),
256
     * ] );
257
     *
258
     * Timber::render('single-form.twig', $context);
259
     * ```
260
     * ```twig
261
     * <h1>{{ post.title }}</h1>
262
     * {{ my_form }}
263
     * ```
264
     * ```html
265
     * <h1>Apply to my contest!</h1>
266
     * <form action="form.php"><input type="text" /><input type="submit /></form>
267
     * ```
268
     *
269
     * @param callable $function
270
     * @param array    $args
271
     *
272
     * @return string
273
     */
274
    public static function ob_function($function, $args = [null])
7✔
275
    {
276
        \ob_start();
7✔
277
        \call_user_func_array($function, $args);
7✔
278
        return \ob_get_clean();
7✔
279
    }
280

281
    /**
282
     * Output a value (string, array, object, etc.) to the error log
283
     *
284
     * @api
285
     * @return void
286
     */
287
    public static function error_log(mixed $error)
7✔
288
    {
289
        global $timber_disable_error_log;
290
        if (!WP_DEBUG || $timber_disable_error_log) {
7✔
291
            return;
2✔
292
        }
293
        if (\is_object($error) || \is_array($error)) {
5✔
294
            $error = \print_r($error, true);
1✔
295
        }
296
        return \error_log('[ Timber ] ' . $error);
5✔
297
    }
298

299
    /**
300
     * Trigger a warning.
301
     *
302
     * @api
303
     *
304
     * @param string $message The warning that you want to output.
305
     *
306
     * @return void
307
     */
308
    public static function warn($message)
×
309
    {
310
        if (!WP_DEBUG) {
×
311
            return;
×
312
        }
313

314
        \trigger_error($message, E_USER_WARNING);
×
315
    }
316

317
    /**
318
     * Marks something as being incorrectly called.
319
     *
320
     * There is a hook 'doing_it_wrong_run' that will be called that can be used
321
     * to get the backtrace up to what file and function called the deprecated
322
     * function.
323
     *
324
     * The current behavior is to trigger a user error if `WP_DEBUG` is true.
325
     *
326
     * If you want to catch errors like these in tests, then add the @expectedIncorrectUsage tag.
327
     * E.g.: "@expectedIncorrectUsage Timber::get_posts()".
328
     *
329
     * @api
330
     * @since 2.0.0
331
     * @since WordPress 3.1.0
332
     * @see \_doing_it_wrong()
333
     *
334
     * @param string $function The function that was called.
335
     * @param string $message  A message explaining what has been done incorrectly.
336
     * @param string $version  The version of Timber where the message was added.
337
     */
338
    public static function doing_it_wrong($function, $message, $version)
14✔
339
    {
340
        /**
341
         * Fires when the given function is being used incorrectly.
342
         *
343
         * @param string $function The function that was called.
344
         * @param string $message  A message explaining what has been done incorrectly.
345
         * @param string $version  The version of WordPress where the message was added.
346
         */
347
        \do_action('doing_it_wrong_run', $function, $message, $version);
14✔
348

349
        if (!WP_DEBUG) {
14✔
350
            return;
×
351
        }
352

353
        /**
354
         * Filters whether to trigger an error for _doing_it_wrong() calls.
355
         *
356
         * This filter is mainly used by unit tests.
357
         *
358
         * @since WordPress 3.1.0
359
         * @since WordPress 5.1.0 Added the $function, $message and $version parameters.
360
         *
361
         * @param bool   $trigger  Whether to trigger the error for _doing_it_wrong() calls. Default true.
362
         * @param string $function The function that was called.
363
         * @param string $message  A message explaining what has been done incorrectly.
364
         * @param string $version  The version of WordPress where the message was added.
365
         */
366
        $should_trigger_error = \apply_filters(
14✔
367
            'doing_it_wrong_trigger_error',
14✔
368
            true,
14✔
369
            $function,
14✔
370
            $message,
14✔
371
            $version
14✔
372
        );
14✔
373

374
        if ($should_trigger_error) {
14✔
375
            if (\is_null($version)) {
×
376
                $version = '';
×
377
            } else {
378
                $version = \sprintf(
×
379
                    '(This message was added in Timber version %s.)',
×
380
                    $version
×
381
                );
×
382
            }
383

384
            $message .= \sprintf(
×
385
                ' Please see Debugging in WordPress (%1$s) as well as Debugging in Timber (%2$s) for more information.',
×
386
                'https://wordpress.org/support/article/debugging-in-wordpress/',
×
387
                'https://timber.github.io/docs/v2/guides/debugging/'
×
388
            );
×
389

390
            $error_message = \sprintf(
×
391
                '%1$s was called <strong>incorrectly</strong>. %2$s %3$s',
×
392
                $function,
×
393
                $message,
×
394
                $version
×
395
            );
×
396

397
            // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
398
            // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
399
            \trigger_error('[ Timber ] ' . $error_message);
×
400
        }
401
    }
402

403
    /**
404
     * Triggers a deprecation warning.
405
     *
406
     * If you want to catch errors like these in tests, then add the @expectedDeprecated tag to the
407
     * DocBlock. E.g.: "@expectedDeprecated {{ TimberImage() }}".
408
     *
409
     * @api
410
     * @see \_deprecated_function()
411
     *
412
     * @param string $function    The name of the deprecated function/method.
413
     * @param string $replacement The name of the function/method to use instead.
414
     * @param string $version     The version of Timber when the function was deprecated.
415
     *
416
     * @return void
417
     */
418
    public static function deprecated($function, $replacement, $version)
58✔
419
    {
420
        /**
421
         * Fires when a deprecated function is being used.
422
         *
423
         * @param string $function    The function that was called.
424
         * @param string $replacement The name of the function/method to use instead.
425
         * @param string $version     The version of Timber where the message was added.
426
         */
427
        \do_action('deprecated_function_run', $function, $replacement, $version);
58✔
428

429
        if (!WP_DEBUG) {
58✔
430
            return;
×
431
        }
432

433
        /**
434
         * Filters whether to trigger an error for deprecated functions.
435
         *
436
         * @since WordPress 2.5.0
437
         *
438
         * @param bool $trigger Whether to trigger the error for deprecated functions. Default true.
439
         */
440
        if (!\apply_filters('deprecated_function_trigger_error', true)) {
58✔
441
            return;
58✔
442
        }
443

444
        if (!\is_null($replacement)) {
×
445
            $error_message = \sprintf(
×
446
                '%1$s is deprecated since Timber version %2$s! Use %3$s instead.',
×
447
                $function,
×
448
                $version,
×
449
                $replacement
×
450
            );
×
451
        } else {
452
            $error_message = \sprintf(
×
453
                '%1$s is deprecated since Timber version %2$s with no alternative available.',
×
454
                $function,
×
455
                $version
×
456
            );
×
457
        }
458

459
        // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
460
        // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
461
        \trigger_error('[ Timber ] ' . $error_message, \E_USER_DEPRECATED);
×
462
    }
463

464
    /**
465
     * @api
466
     *
467
     * @param string  $separator
468
     * @param string  $seplocation
469
     * @return string
470
     */
471
    public static function get_wp_title($separator = ' ', $seplocation = 'left')
45✔
472
    {
473
        /**
474
         * Filters the separator used for the page title.
475
         *
476
         * @since 2.0.0
477
         *
478
         * @param string $separator The separator to use. Default `' '`.
479
         */
480
        $separator = \apply_filters('timber/helper/wp_title_separator', $separator);
45✔
481

482
        /**
483
         * Filters the separator used for the page title.
484
         *
485
         * @deprecated 2.0.0, use `timber/helper/wp_title_separator`
486
         */
487
        $separator = \apply_filters_deprecated('timber_wp_title_seperator', [$separator], '2.0.0', 'timber/helper/wp_title_separator');
45✔
488

489
        return \trim((string) \wp_title($separator, false, $seplocation));
45✔
490
    }
491

492
    /**
493
     * Sorts object arrays by properties.
494
     *
495
     * @api
496
     *
497
     * @param array  $array The array of objects to sort.
498
     * @param string $prop  The property to sort by.
499
     *
500
     * @return void
501
     */
502
    public static function osort(&$array, $prop)
1✔
503
    {
504
        \usort($array, fn ($a, $b) => $a->$prop > $b->$prop ? 1 : -1);
1✔
505
    }
506

507
    /**
508
     * @api
509
     *
510
     * @param array   $arr
511
     * @return bool
512
     */
513
    public static function is_array_assoc($arr)
1✔
514
    {
515
        if (!\is_array($arr)) {
1✔
516
            return false;
×
517
        }
518
        return (bool) \count(\array_filter(\array_keys($arr), 'is_string'));
1✔
519
    }
520

521
    /**
522
     * @api
523
     *
524
     * @param array   $array
525
     * @return stdClass
526
     */
527
    public static function array_to_object($array)
2✔
528
    {
529
        $obj = new stdClass();
2✔
530
        foreach ($array as $k => $v) {
2✔
531
            if (\is_array($v)) {
2✔
532
                $obj->{$k} = self::array_to_object($v); //RECURSION
1✔
533
            } else {
534
                $obj->{$k} = $v;
2✔
535
            }
536
        }
537
        return $obj;
2✔
538
    }
539

540
    /**
541
     * @api
542
     *
543
     * @param array   $array
544
     * @param string  $key
545
     * @return bool|int
546
     */
547
    public static function get_object_index_by_property($array, $key, mixed $value)
2✔
548
    {
549
        if (\is_array($array)) {
2✔
550
            $i = 0;
2✔
551
            foreach ($array as $arr) {
2✔
552
                if (\is_array($arr)) {
2✔
553
                    if ($arr[$key] == $value) {
1✔
554
                        return $i;
1✔
555
                    }
556
                } else {
557
                    if ($arr->$key == $value) {
1✔
558
                        return $i;
1✔
559
                    }
560
                }
561
                $i++;
2✔
562
            }
563
        }
564
        return false;
1✔
565
    }
566

567
    /**
568
     * @api
569
     *
570
     * @param array   $array
571
     * @param string  $key
572
     * @return array|null
573
     * @throws Exception
574
     */
575
    public static function get_object_by_property($array, $key, mixed $value)
3✔
576
    {
577
        if (\is_array($array)) {
3✔
578
            foreach ($array as $arr) {
2✔
579
                if ($arr->$key == $value) {
2✔
580
                    return $arr;
1✔
581
                }
582
            }
583
            return false;
1✔
584
        }
585
        throw new InvalidArgumentException('$array is not an array, got:');
1✔
586
    }
587

588
    /**
589
     * @api
590
     *
591
     * @param array   $array
592
     * @param int     $len
593
     * @return array
594
     */
595
    public static function array_truncate($array, $len)
1✔
596
    {
597
        if (\sizeof($array) > $len) {
1✔
598
            $array = \array_splice($array, 0, $len);
1✔
599
        }
600
        return $array;
1✔
601
    }
602

603
    /* Bool Utilities
604
       ======================== */
605
    /**
606
     * @api
607
     *
608
     * @return bool
609
     */
610
    public static function is_true(mixed $value)
2✔
611
    {
612
        if (isset($value)) {
2✔
613
            if (\is_string($value)) {
2✔
614
                $value = \strtolower($value);
2✔
615
            }
616
            if (($value == 'true' || $value === 1 || $value === '1' || $value == true) && $value !== false && $value !== 'false') {
2✔
617
                return true;
2✔
618
            }
619
        }
620
        return false;
2✔
621
    }
622

623
    /**
624
     * Is the number even? Let's find out.
625
     *
626
     * @api
627
     *
628
     * @param int $i number to test.
629
     * @return bool
630
     */
631
    public static function iseven($i)
1✔
632
    {
633
        return ($i % 2) === 0;
1✔
634
    }
635

636
    /**
637
     * Is the number odd? Let's find out.
638
     *
639
     * @api
640
     *
641
     * @param int $i number to test.
642
     * @return bool
643
     */
644
    public static function isodd($i)
1✔
645
    {
646
        return ($i % 2) !== 0;
1✔
647
    }
648

649
    /**
650
     * Plucks the values of a certain key from an array of objects
651
     *
652
     * @api
653
     *
654
     * @param array  $array
655
     * @param string $key
656
     *
657
     * @return array
658
     */
659
    public static function pluck($array, $key)
4✔
660
    {
661
        $return = [];
4✔
662
        foreach ($array as $obj) {
4✔
663
            if (\is_object($obj) && \method_exists($obj, $key)) {
4✔
664
                $return[] = $obj->$key();
1✔
665
            } elseif (\is_object($obj) && \property_exists($obj, $key)) {
4✔
666
                $return[] = $obj->$key;
1✔
667
            } elseif (\is_array($obj) && isset($obj[$key])) {
3✔
668
                $return[] = $obj[$key];
2✔
669
            }
670
        }
671
        return $return;
4✔
672
    }
673

674
    /**
675
     * Filters a list of objects, based on a set of key => value arguments.
676
     * Uses WordPress WP_List_Util's filter.
677
     *
678
     * @api
679
     * @since 1.5.3
680
     * @ticket #1594
681
     *
682
     * @param array        $list to filter.
683
     * @param string|array $args to search for.
684
     * @param string       $operator to use (AND, NOT, OR).
685
     * @return array
686
     */
687
    public static function wp_list_filter($list, $args, $operator = 'AND')
1✔
688
    {
689
        if (!\is_array($args)) {
1✔
690
            $args = [
1✔
691
                'slug' => $args,
1✔
692
            ];
1✔
693
        }
694

695
        if (!\is_array($list) && !\is_a($list, 'Traversable')) {
1✔
696
            return [];
×
697
        }
698

699
        $util = new WP_List_Util($list);
1✔
700
        return $util->filter($args, $operator);
1✔
701
    }
702

703
    /**
704
     * Converts a WP object (WP_Post, WP_Term) into its
705
     * equivalent Timber class (Timber\Post, Timber\Term).
706
     *
707
     * If no match is found the function will return the initial argument.
708
     *
709
     * @api
710
     * @param mixed $obj WP Object to convert
711
     * @return mixed Instance of equivalent Timber object, or the argument if no match is found
712
     */
713
    public static function convert_wp_object(mixed $obj)
17✔
714
    {
715
        if ($obj instanceof WP_Post) {
17✔
716
            static $postFactory;
6✔
717
            $postFactory = $postFactory ?: new PostFactory();
6✔
718
            return $postFactory->from($obj->ID);
6✔
719
        } elseif ($obj instanceof WP_Term) {
13✔
720
            return Timber::get_term($obj->term_id);
1✔
721
        } elseif ($obj instanceof WP_User) {
13✔
722
            return Timber::get_user($obj->ID);
1✔
723
        }
724

725
        return $obj;
13✔
726
    }
727
}
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