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

timber / timber / 15920925840

27 Jun 2025 07:39AM UTC coverage: 88.19%. Remained the same
15920925840

push

github

web-flow
chore(deps): update .lock file and update ci steps

* chore(deps): update package versions in composer.lock
* ci: run ci also on .lock file
* chore(deps): remove deprecated twig/cache-extension dependency
* chore: add step to remove composer.lock before installing dependencies
* chore: fix shell command syntax for removing composer.lock

3883 of 4403 relevant lines covered (88.19%)

64.55 hits per line

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

80.85
/src/MenuItem.php
1
<?php
2

3
namespace Timber;
4

5
use stdClass;
6
use Stringable;
7
use Timber\Factory\PostFactory;
8
use Timber\Factory\TermFactory;
9
use WP_Post;
10

11
/**
12
 * Class MenuItem
13
 *
14
 * @api
15
 */
16
class MenuItem extends CoreEntity implements Stringable
17
{
18
    /**
19
     * @var string What does this class represent in WordPress terms?
20
     */
21
    public $object_type = 'post';
22

23
    /**
24
     * @api
25
     * @var array Array of children of a menu item. Empty if there are no child menu items.
26
     */
27
    public $children = [];
28

29
    /**
30
     * @api
31
     * @var array Array of class names.
32
     */
33
    public $classes = [];
34

35
    public $class = '';
36

37
    public $level = 0;
38

39
    public $post_name;
40

41
    public $url;
42

43
    public $type;
44

45
    /**
46
     * Protected is needed here since we want to force Twig to use the `title()` method
47
     * in order to apply the `nav_menu_item_title` filter
48
     */
49
    protected $title = '';
50

51
    /**
52
     * Inherited property. Listed here to make it available in the documentation.
53
     *
54
     * @api
55
     * @see _wp_menu_item_classes_by_context()
56
     * @var bool Whether the menu item links to the currently displayed page.
57
     */
58
    public $current;
59

60
    /**
61
     * Inherited property. Listed here to make it available in the documentation.
62
     *
63
     * @api
64
     * @see _wp_menu_item_classes_by_context()
65
     * @var bool Whether the menu item refers to the parent item of the currently displayed page.
66
     */
67
    public $current_item_parent;
68

69
    /**
70
     * Inherited property. Listed here to make it available in the documentation.
71
     *
72
     * @api
73
     * @see _wp_menu_item_classes_by_context()
74
     * @var bool Whether the menu item refers to an ancestor (including direct parent) of the
75
     *      currently displayed page.
76
     */
77
    public $current_item_ancestor;
78

79
    /**
80
     * Object ID.
81
     *
82
     * @api
83
     * @since 2.0.0
84
     * @var int|null Linked object ID.
85
     */
86
    public $object_id = null;
87

88
    /**
89
     * Object type.
90
     *
91
     * @api
92
     * @since 2.0.0
93
     * @var string The underlying menu object type. E.g. a post type name, a taxonomy name or 'custom'.
94
     */
95
    public $object;
96

97
    protected $_name;
98

99
    protected $_menu_item_url;
100

101
    /**
102
     * @internal
103
     * @param array|object $data The data this MenuItem is wrapping
104
     * @param Menu $menu The `Menu` object the menu item is associated with.
105
     * @return static a new MenuItem instance
106
     */
107
    public static function build($data, ?Menu $menu = null): static
108
    {
109
        return new static($data, $menu);
60✔
110
    }
111

112
    /**
113
     * @internal
114
     * @param Menu $menu The `Menu` object the menu item is associated with.
115
     */
116
    protected function __construct(
117
        /**
118
         * The underlying WordPress Core object.
119
         *
120
         * @since 2.0.0
121
         */
122
        protected ?WP_Post $wp_object,
123
        /**
124
         * Timber Menu. Previously this was a public property, but converted to a method to avoid
125
         * recursion (see #2071).
126
         *
127
         * @since 1.12.0
128
         * @see \Timber\MenuItem::menu()
129
         */
130
        protected $menu = null
131
    ) {
132
        /**
133
         * @property string $title The nav menu item title.
134
         */
135
        // @phpstan-ignore property.notFound
136
        $this->title = $this->wp_object->title;
60✔
137

138
        $this->import($this->wp_object);
60✔
139
        $this->import_classes($this->wp_object);
60✔
140
        $this->id = $this->wp_object->ID;
60✔
141
        $this->ID = $this->wp_object->ID;
60✔
142

143
        $this->_name = $this->wp_object->name ?? '';
60✔
144
        $this->add_class('menu-item-' . $this->ID);
60✔
145

146
        /**
147
         * Because init_as_page_menu already set it to simulate the master object
148
         *
149
         * @see Menu::init_as_page_menu
150
         */
151
        if (!isset($this->object_id)) {
60✔
152
            $this->object_id = (int) \get_post_meta($this->ID, '_menu_item_object_id', true);
4✔
153
        }
154
    }
155

156
    /**
157
     * Gets the underlying WordPress Core object.
158
     *
159
     * @since 2.0.0
160
     *
161
     * @return WP_Post|null
162
     */
163
    public function wp_object(): ?WP_Post
164
    {
165
        return $this->wp_object;
1✔
166
    }
167

168
    /**
169
     * Add a CSS class the menu item should have.
170
     *
171
     * @param string $class_name CSS class name to be added.
172
     */
173
    public function add_class(string $class_name)
174
    {
175
        // Class name is already there
176
        if (\in_array($class_name, $this->classes, true)) {
60✔
177
            return;
×
178
        }
179
        $this->classes[] = $class_name;
60✔
180
        $this->update_class();
60✔
181
    }
182

183
    /**
184
     * Add a CSS class the menu item should have.
185
     *
186
     * @param string $class_name CSS class name to be added.
187
     */
188
    public function remove_class(string $class_name)
189
    {
190
        // Class name is already there
191
        if (!\in_array($class_name, $this->classes, true)) {
6✔
192
            return;
6✔
193
        }
194
        $class_key = \array_search($class_name, $this->classes, true);
1✔
195
        unset($this->classes[$class_key]);
1✔
196
        $this->update_class();
1✔
197
    }
198

199
    /**
200
     * Update class string
201
     */
202
    protected function update_class()
203
    {
204
        $this->class = \trim(\implode(' ', $this->classes));
60✔
205
    }
206

207
    /**
208
     * Get the label for the menu item.
209
     *
210
     * @api
211
     * @return string The label for the menu item.
212
     */
213
    public function name()
214
    {
215
        return $this->title();
1✔
216
    }
217

218
    /**
219
     * Magic method to get the label for the menu item.
220
     *
221
     * @api
222
     * @example
223
     * ```twig
224
     * <a href="{{ item.link }}">{{ item }}</a>
225
     * ```
226
     * @see \Timber\MenuItem::name()
227
     * @return string The label for the menu item.
228
     */
229
    public function __toString(): string
230
    {
231
        return $this->name();
1✔
232
    }
233

234
    /**
235
     * Get the slug for the menu item.
236
     *
237
     * @api
238
     * @example
239
     * ```twig
240
     * <ul>
241
     *     {% for item in menu.items %}
242
     *         <li class="{{ item.slug }}">
243
     *             <a href="{{ item.link }}">{{ item.name }}</a>
244
     *          </li>
245
     *     {% endfor %}
246
     * </ul>
247
     * ```
248
     * @return string The URL-safe slug of the menu item.
249
     */
250
    public function slug()
251
    {
252
        $mo = $this->master_object();
1✔
253
        if ($mo && $mo->post_name) {
1✔
254
            return $mo->post_name;
1✔
255
        }
256
        return $this->post_name;
×
257
    }
258

259
    /**
260
     * Allows dev to access the "master object" (ex: post, page, category, post type object) the menu item represents
261
     *
262
     * @api
263
     * @example
264
     * ```twig
265
     * <div>
266
     *     {% for item in menu.items %}
267
     *         <a href="{{ item.link }}"><img src="{{ item.master_object.thumbnail }}" /></a>
268
     *     {% endfor %}
269
     * </div>
270
     * ```
271
     * @return mixed|null Whatever object (Timber\Post, Timber\Term, etc.) the menu item represents.
272
     */
273
    public function master_object()
274
    {
275
        switch ($this->type) {
5✔
276
            case 'post_type':
5✔
277
                $factory = new PostFactory();
5✔
278
                break;
5✔
279
            case 'taxonomy':
1✔
280
                $factory = new TermFactory();
1✔
281
                break;
1✔
282
            case 'post_type_archive':
1✔
283
                return \get_post_type_object($this->object);
1✔
284
            default:
285
                $factory = null;
×
286
                break;
×
287
        }
288

289
        return $factory && $this->object_id ? $factory->from($this->object_id) : null;
5✔
290
    }
291

292
    /**
293
     * Add a new `Timber\MenuItem` object as a child of this menu item.
294
     *
295
     * @api
296
     *
297
     * @param MenuItem $item The menu item to add.
298
     */
299
    public function add_child(MenuItem $item)
300
    {
301
        $this->children[] = $item;
39✔
302
        $item->level = $this->level + 1;
39✔
303
        if (\count($this->children)) {
39✔
304
            $this->update_child_levels();
39✔
305
        }
306
    }
307

308
    /**
309
     * Update the level data associated with $this.
310
     *
311
     * @internal
312
     * @return bool|null
313
     */
314
    public function update_child_levels()
315
    {
316
        if (\is_array($this->children)) {
39✔
317
            foreach ($this->children as $child) {
39✔
318
                $child->level = $this->level + 1;
39✔
319
                $child->update_child_levels();
39✔
320
            }
321
            return true;
39✔
322
        }
323
    }
324

325
    /**
326
     * Imports the classes to be used in CSS.
327
     *
328
     * @internal
329
     *
330
     * @param array|object $data to import.
331
     */
332
    public function import_classes($data)
333
    {
334
        if (\is_array($data)) {
60✔
335
            $data = (object) $data;
1✔
336
        }
337
        $this->classes = \array_unique(\array_merge($this->classes, $data->classes ?? []));
60✔
338
        $this->classes = \array_values(\array_filter($this->classes));
60✔
339

340
        $args = new stdClass();
60✔
341
        if (isset($this->menu->args)) {
60✔
342
            // The args need to be an object.
343
            $args = $this->menu->args;
59✔
344
        }
345

346
        /**
347
         * @see Walker_Nav_Menu
348
         */
349
        $this->classes = \apply_filters(
60✔
350
            'nav_menu_css_class',
60✔
351
            $this->classes,
60✔
352
            $this->wp_object,
60✔
353
            $args,
60✔
354
            0 // TODO: find the right depth
60✔
355
        );
60✔
356

357
        $this->update_class();
60✔
358
    }
359

360
    /**
361
     * Get children of a menu item.
362
     *
363
     * You can also directly access the children through the `$children` property (`item.children`
364
     * in Twig).
365
     *
366
     * @internal
367
     * @deprecated 2.0.0, use `item.children` instead.
368
     * @example
369
     * ```twig
370
     * {% for child in item.get_children %}
371
     *     <li class="nav-drop-item">
372
     *         <a href="{{ child.link }}">{{ child.title }}</a>
373
     *     </li>
374
     * {% endfor %}
375
     * ```
376
     * @return array|bool Array of children of a menu item. Empty if there are no child menu items.
377
     */
378
    public function get_children()
379
    {
380
        Helper::deprecated(
×
381
            "{{ item.get_children }}",
×
382
            "{{ item.children }}",
×
383
            '2.0.0'
×
384
        );
×
385
        return $this->children();
×
386
    }
387

388
    /**
389
     * Checks to see if the menu item is an external link.
390
     *
391
     * If your site is `example.org`, then `google.com/whatever` is an external link. This is
392
     * helpful when you want to style external links differently or create rules for the target of a
393
     * link.
394
     *
395
     * @api
396
     * @example
397
     * ```twig
398
     * <a href="{{ item.link }}" target="{{ item.is_external ? '_blank' : '_self' }}">
399
     * ```
400
     *
401
     * Or when you only want to add a target attribute if it is really needed:
402
     *
403
     * ```twig
404
     * <a href="{{ item.link }}" {{ item.is_external ? 'target="_blank"' }}>
405
     * ```
406
     *
407
     * In combination with `is_target_blank()`:
408
     *
409
     * ```twig
410
     * <a href="{{ item.link }}" {{ item.is_external or item.is_target_blank ? 'target="_blank"' }}>
411
     * ```
412
     *
413
     * @return bool Whether the link is external or not.
414
     */
415
    public function is_external()
416
    {
417
        if ($this->type !== 'custom') {
2✔
418
            return false;
1✔
419
        }
420

421
        // Additional check for relative/non-URLs
422
        if (false === URLHelper::is_url($this->link())) {
1✔
423
            return false;
1✔
424
        }
425

426
        return URLHelper::is_external($this->link());
1✔
427
    }
428

429
    /**
430
     * Checks whether the «Open in new tab» option checked in the menu item options.
431
     *
432
     * @example
433
     * ```twig
434
     * <a href="{{ item.link }}" {{ item.is_target_blank ? 'target="_blank"' }}>
435
     * ```
436
     *
437
     * In combination with `is_external()`
438
     *
439
     * ```twig
440
     * <a href="{{ item.link }}" {{ item.is_target_blank or item.is_external ? 'target="_blank"' }}>
441
     * ```
442
     *
443
     * @return bool Whether the menu item has the «Open in new tab» option checked in the menu item
444
     *              options.
445
     */
446
    public function is_target_blank()
447
    {
448
        return '_blank' === $this->meta('_menu_item_target');
1✔
449
    }
450

451
    /**
452
     * Gets the target of a menu item according to the «Open in new tab» option in the menu item
453
     * options.
454
     *
455
     * This function return `_blank` when the option to open a menu item in a new tab is checked in
456
     * the WordPress backend, and `_self` if the option is not checked. Beware `_self` is the
457
     * default value for the target attribute, which means you could leave it out. You can use
458
     * `item.is_target_blank` if you want to use a conditional.
459
     *
460
     * @example
461
     * ```twig
462
     * <a href="{{ item.link }}" target="{{ item.target }}">
463
     * ```
464
     *
465
     * @return string
466
     */
467
    public function target()
468
    {
469
        $target = $this->meta('_menu_item_target');
1✔
470
        if (!$target) {
1✔
471
            return '_self';
1✔
472
        }
473
        return $target;
1✔
474
    }
475

476
    /**
477
     * Timber Menu.
478
     *
479
     * @api
480
     * @since 1.12.0
481
     * @return Menu The `Menu` object the menu item is associated with.
482
     */
483
    public function menu()
484
    {
485
        return $this->menu;
2✔
486
    }
487

488
    /**
489
     * Gets a menu item meta value.
490
     *
491
     * @api
492
     * @deprecated 2.0.0, use `{{ item.meta('field_name') }}` instead.
493
     * @see \Timber\MenuItem::meta()
494
     *
495
     * @param string $field_name The field name for which you want to get the value.
496
     * @return mixed The meta field value.
497
     */
498
    public function get_field($field_name = null)
499
    {
500
        Helper::deprecated(
×
501
            "{{ item.get_field('field_name') }}",
×
502
            "{{ item.meta('field_name') }}",
×
503
            '2.0.0'
×
504
        );
×
505
        return $this->meta($field_name);
×
506
    }
507

508
    /**
509
     * Get the child menu items of a `Timber\MenuItem`.
510
     *
511
     * @api
512
     * @example
513
     * ```twig
514
     * {% for child in item.children %}
515
     *     <li class="nav-drop-item">
516
     *         <a href="{{ child.link }}">{{ child.title }}</a>
517
     *     </li>
518
     * {% endfor %}
519
     * ```
520
     * @return array|bool Array of children of a menu item. Empty if there are no child menu items.
521
     */
522
    public function children()
523
    {
524
        return $this->children;
7✔
525
    }
526

527
    /**
528
     * Checks to see if the menu item is an external link.
529
     *
530
     * @api
531
     * @deprecated 2.0.0, use `{{ item.is_external }}`
532
     * @see \Timber\MenuItem::is_external()
533
     *
534
     * @return bool Whether the link is external or not.
535
     */
536
    public function external()
537
    {
538
        Helper::warn('{{ item.external }} is deprecated. Use {{ item.is_external }} instead.');
×
539
        return $this->is_external();
×
540
    }
541

542
    /**
543
     * Get the full link to a menu item.
544
     *
545
     * @api
546
     * @example
547
     * ```twig
548
     * {% for item in menu.items %}
549
     *     <li><a href="{{ item.link }}">{{ item.title }}</a></li>
550
     * {% endfor %}
551
     * ```
552
     * @return string A full URL, like `https://mysite.com/thing/`.
553
     */
554
    public function link()
555
    {
556
        return $this->url;
13✔
557
    }
558

559
    /**
560
     * Get the relative path of the menu item’s link.
561
     *
562
     * @api
563
     * @example
564
     * ```twig
565
     * {% for item in menu.items %}
566
     *     <li><a href="{{ item.path }}">{{ item.title }}</a></li>
567
     * {% endfor %}
568
     * ```
569
     * @return string The path of a URL, like `/foo`.
570
     */
571
    public function path()
572
    {
573
        return URLHelper::get_rel_url($this->link());
4✔
574
    }
575

576
    /**
577
     * Get the public label for the menu item.
578
     *
579
     * @api
580
     * @example
581
     * ```twig
582
     * {% for item in menu.items %}
583
     *     <li><a href="{{ item.link }}">{{ item.title }}</a></li>
584
     * {% endfor %}
585
     * ```
586
     * @return string The public label, like "Foo".
587
     */
588
    public function title()
589
    {
590
        /**
591
         * @see Walker_Nav_Menu::start_el()
592
         */
593
        $title = \apply_filters('nav_menu_item_title', $this->title, $this->wp_object, $this->menu->args ?: new stdClass(), $this->level);
8✔
594
        return $title;
8✔
595
    }
596

597
    /**
598
     * Checks whether the current user can edit the menu item.
599
     *
600
     * @api
601
     * @since 2.0.0
602
     * @return bool
603
     */
604
    public function can_edit(): bool
605
    {
606
        return \current_user_can('edit_theme_options');
1✔
607
    }
608
}
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

© 2025 Coveralls, Inc