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

timber / timber / 5690593717

pending completion
5690593717

Pull #1617

github

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

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

3931 of 4433 relevant lines covered (88.68%)

58.28 hits per line

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

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

3
namespace Timber;
4

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

10
/**
11
 * Class MenuItem
12
 *
13
 * @api
14
 */
15
class MenuItem extends CoreEntity
16
{
17
    /**
18
     * The underlying WordPress Core object.
19
     *
20
     * @since 2.0.0
21
     *
22
     * @var WP_Post|null
23
     */
24
    protected ?WP_Post $wp_object;
25

26
    /**
27
     * @var string What does this class represent in WordPress terms?
28
     */
29
    public $object_type = 'post';
30

31
    /**
32
     * @api
33
     * @var array Array of children of a menu item. Empty if there are no child menu items.
34
     */
35
    public $children = [];
36

37
    /**
38
     * @api
39
     * @var array Array of class names.
40
     */
41
    public $classes = [];
42

43
    public $class = '';
44

45
    public $level = 0;
46

47
    public $post_name;
48

49
    public $url;
50

51
    public $type;
52

53
    /**
54
     * Protected is needed here since we want to force Twig to use the `title()` method
55
     * in order to apply the `nav_menu_item_title` filter
56
     */
57
    protected $title = '';
58

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

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

77
    /**
78
     * Inherited property. Listed here to make it available in the documentation.
79
     *
80
     * @api
81
     * @see _wp_menu_item_classes_by_context()
82
     * @var bool Whether the menu item refers to an ancestor (including direct parent) of the
83
     *      currently displayed page.
84
     */
85
    public $current_item_ancestor;
86

87
    /**
88
     * Timber Menu. Previously this was a public property, but converted to a method to avoid
89
     * recursion (see #2071).
90
     *
91
     * @since 1.12.0
92
     * @see \Timber\MenuItem::menu()
93
     * @var \Timber\Menu The `Timber\Menu` object the menu item is associated with.
94
     */
95
    protected $menu;
96

97
    /**
98
     * Object ID.
99
     *
100
     * @api
101
     * @since 2.0.0
102
     * @var int|null Linked object ID.
103
     */
104
    public $object_id = null;
105

106
    /**
107
     * Object type.
108
     *
109
     * @api
110
     * @since 2.0.0
111
     * @var string The underlying menu object type. E.g. a post type name, a taxonomy name or 'custom'.
112
     */
113
    public $object;
114

115
    protected $_name;
116

117
    protected $_menu_item_url;
118

119
    /**
120
     * @internal
121
     * @param array|object $data The data this MenuItem is wrapping
122
     * @param \Timber\Menu $menu The `Timber\Menu` object the menu item is associated with.
123
     * @return \Timber\MenuItem a new MenuItem instance
124
     */
125
    public static function build($data, ?Menu $menu = null): self
126
    {
127
        return new static($data, $menu);
62✔
128
    }
129

130
    /**
131
     * @internal
132
     * @param WP_Post $data
133
     * @param \Timber\Menu $menu The `Timber\Menu` object the menu item is associated with.
134
     */
135
    final protected function __construct(WP_Post $data, $menu = null)
136
    {
137
        $this->wp_object = $data;
62✔
138
        $this->menu = $menu;
62✔
139

140
        /**
141
         * @property string $title The nav menu item title.
142
         */
143
        $this->title = $data->title;
62✔
144

145
        $this->import($data);
62✔
146
        $this->import_classes($data);
62✔
147
        $this->id = $data->ID;
62✔
148
        $this->ID = $data->ID;
62✔
149

150
        $this->_name = $data->name ?? '';
62✔
151
        $this->add_class('menu-item-' . $this->ID);
62✔
152

153
        /**
154
         * Because init_as_page_menu already set it to simulate the master object
155
         *
156
         * @see Menu::init_as_page_menu
157
         */
158
        if (!isset($this->object_id)) {
62✔
159
            $this->object_id = (int) \get_post_meta($this->ID, '_menu_item_object_id', true);
4✔
160
        }
161
    }
162

163
    /**
164
     * Gets the underlying WordPress Core object.
165
     *
166
     * @since 2.0.0
167
     *
168
     * @return WP_Post|null
169
     */
170
    public function wp_object(): ?WP_Post
171
    {
172
        return $this->wp_object;
1✔
173
    }
174

175
    /**
176
     * Add a CSS class the menu item should have.
177
     *
178
     * @param string $class_name CSS class name to be added.
179
     */
180
    public function add_class(string $class_name)
181
    {
182
        // Class name is already there
183
        if (!\in_array($class_name, $this->classes, true)) {
62✔
184
            return;
62✔
185
        }
186
        $this->classes[] = $class_name;
×
187
        $this->update_class();
×
188
    }
189

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

206
    /**
207
     * Update class string
208
     */
209
    protected function update_class()
210
    {
211
        $this->class = \trim(\implode(' ', $this->classes));
62✔
212
    }
213

214
    /**
215
     * Get the label for the menu item.
216
     *
217
     * @api
218
     * @return string The label for the menu item.
219
     */
220
    public function name()
221
    {
222
        return $this->title();
1✔
223
    }
224

225
    /**
226
     * Magic method to get the label for the menu item.
227
     *
228
     * @api
229
     * @example
230
     * ```twig
231
     * <a href="{{ item.link }}">{{ item }}</a>
232
     * ```
233
     * @see \Timber\MenuItem::name()
234
     * @return string The label for the menu item.
235
     */
236
    public function __toString()
237
    {
238
        return $this->name();
1✔
239
    }
240

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

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

296
        return $factory && $this->object_id ? $factory->from($this->object_id) : null;
6✔
297
    }
298

299
    /**
300
     * Add a new `Timber\MenuItem` object as a child of this menu item.
301
     *
302
     * @api
303
     *
304
     * @param \Timber\MenuItem $item The menu item to add.
305
     */
306
    public function add_child(MenuItem $item)
307
    {
308
        $this->children[] = $item;
40✔
309
        $item->level = $this->level + 1;
40✔
310
        if (\count($this->children)) {
40✔
311
            $this->update_child_levels();
40✔
312
        }
313
    }
314

315
    /**
316
     * Update the level data associated with $this.
317
     *
318
     * @internal
319
     * @return bool|null
320
     */
321
    public function update_child_levels()
322
    {
323
        if (\is_array($this->children)) {
40✔
324
            foreach ($this->children as $child) {
40✔
325
                $child->level = $this->level + 1;
40✔
326
                $child->update_child_levels();
40✔
327
            }
328
            return true;
40✔
329
        }
330
    }
331

332
    /**
333
     * Imports the classes to be used in CSS.
334
     *
335
     * @internal
336
     *
337
     * @param array|object $data to import.
338
     */
339
    public function import_classes($data)
340
    {
341
        if (\is_array($data)) {
62✔
342
            $data = (object) $data;
1✔
343
        }
344
        $this->classes = \array_unique(\array_merge($this->classes, $data->classes ?? []));
62✔
345
        $this->classes = \array_values(\array_filter($this->classes));
62✔
346

347
        $args = new stdClass();
62✔
348
        if (isset($this->menu->args)) {
62✔
349
            // The args need to be an object.
350
            $args = $this->menu->args;
61✔
351
        }
352

353
        /**
354
         * @see Walker_Nav_Menu
355
         */
356
        $this->classes = \apply_filters(
62✔
357
            'nav_menu_css_class',
62✔
358
            $this->classes,
62✔
359
            $this->wp_object,
62✔
360
            $args,
62✔
361
            0 // TODO: find the right depth
62✔
362
        );
62✔
363

364
        $this->update_class();
62✔
365
    }
366

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

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

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

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

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

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

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

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

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

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

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

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