Passed
Pull Request — master (#31)
by Wilmer
02:00
created

Nav::renderItem()   C

Complexity

Conditions 12
Paths 145

Size

Total Lines 70
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 39
CRAP Score 12

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 12
eloc 40
nc 145
nop 1
dl 0
loc 70
ccs 39
cts 39
cp 1
crap 12
rs 6.5916
c 2
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Bulma;
6
7
use InvalidArgumentException;
8
use JsonException;
9
use Yiisoft\Arrays\ArrayHelper;
10
use Yiisoft\Html\Html;
11
12
use function array_key_exists;
13
use function implode;
14
use function is_array;
15
16
final class Nav extends Widget
17
{
18
    private bool $activateItems = true;
19
    private bool $activateParents = false;
20
    private string $currentPath = '';
21
    private bool $encodeLabels = true;
22
    private bool $encodeTags = false;
23
    private array $items = [];
24
25 19
    protected function run(): string
26
    {
27 19
        $items = [];
28
29 19
        foreach ($this->items as $item) {
30 19
            if (!isset($item['visible']) || $item['visible']) {
31 19
                $items[] = $this->renderItem($item);
32
            }
33
        }
34
35 17
        return implode("\n", $items);
36
    }
37
38
    /**
39
     * Disable activate items according to whether their currentPath.
40
     *
41
     * @return $this
42
     *
43
     * {@see isItemActive}
44
     */
45 2
    public function withoutActivateItems(): self
46
    {
47 2
        $new = clone $this;
48 2
        $new->activateItems = false;
49 2
        return $new;
50
    }
51
52
    /**
53
     * Whether to activate parent menu items when one of the corresponding child menu items is active.
54
     *
55
     * @return $this
56
     */
57 1
    public function withActivateParents(): self
58
    {
59 1
        $new = clone $this;
60 1
        $new->activateParents = true;
61 1
        return $new;
62
    }
63
64
    /**
65
     * Allows you to assign the current path of the url from request controller.
66
     *
67
     * @param string $value
68
     *
69
     * @return self
70
     */
71 2
    public function currentPath(string $value): self
72
    {
73 2
        $new = clone $this;
74 2
        $new->currentPath = $value;
75 2
        return $new;
76
    }
77
78
    /**
79
     * When tags Labels HTML should not be encoded.
80
     *
81
     * @return $this
82
     */
83 1
    public function withoutEncodeLabels(): self
84
    {
85 1
        $new = clone $this;
86 1
        $new->encodeLabels = false;
87 1
        return $new;
88
    }
89
90
    /**
91
     * List of items in the nav widget. Each array element represents a single  menu item which can be either a string
92
     * or an array with the following structure:
93
     *
94
     * - label: string, required, the nav item label.
95
     * - url: optional, the item's URL. Defaults to "#".
96
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
97
     * - linkOptions: array, optional, the HTML attributes of the item's link.
98
     * - options: array, optional, the HTML attributes of the item container (LI).
99
     * - active: bool, optional, whether the item should be on active state or not.
100
     * - dropdownOptions: array, optional, the HTML options that will passed to the {@see Dropdown} widget.
101
     * - items: array|string, optional, the configuration array for creating a {@see Dropdown} widget, or a string
102
     *   representing the dropdown menu.
103
     * - encode: bool, optional, whether the label will be HTML-encoded. If set, supersedes the $encodeLabels option for
104
     *   only this item.
105
     *
106
     * If a menu item is a string, it will be rendered directly without HTML encoding.
107
     *
108
     * @param array $value
109
     *
110
     * @return self
111
     */
112 19
    public function items(array $value): self
113
    {
114 19
        $new = clone $this;
115 19
        $new->items = $value;
116 19
        return $new;
117
    }
118
119
    /**
120
     * Allows you to enable the encoding tags html.
121
     *
122
     * @return self
123
     */
124 1
    public function withEncodeTags(): self
125
    {
126 1
        $new = clone $this;
127 1
        $new->encodeTags = true;
128
129 1
        return $new;
130
    }
131
132
    /**
133
     * Renders the given items as a dropdown.
134
     *
135
     * This method is called to create sub-menus.
136
     *
137
     * @param array $items the given items. Please refer to {@see Dropdown::items} for the array structure.
138
     * @param array $parentItem the parent item information. Please refer to {@see items} for the structure of this
139
     * array.
140
     *
141
     * @throws InvalidArgumentException
142
     *
143
     * @return string the rendering result.
144
     */
145 12
    private function renderDropdown(array $items, array $parentItem): string
146
    {
147 12
        $dropdown = Dropdown::widget()
148 12
            ->dividerClass('navbar-divider')
0 ignored issues
show
Bug introduced by
The method dividerClass() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of Yiisoft\Widget\Widget such as Yiisoft\Yii\Bulma\Dropdown. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

148
            ->/** @scrutinizer ignore-call */ dividerClass('navbar-divider')
Loading history...
149 12
            ->itemClass('navbar-item')
150 12
            ->itemsClass('navbar-dropdown')
151 12
            ->withoutEncloseByContainer()
152 12
            ->items($items)
153 12
            ->itemsOptions(ArrayHelper::getValue($parentItem, 'dropdownOptions', []));
154
155 12
        if ($this->encodeLabels === false) {
156 2
            $dropdown = $dropdown->withoutEncodeLabels();
157
        }
158
159 12
        return $dropdown->render() . "\n";
160
    }
161
162
    /**
163
     * Check to see if a child item is active optionally activating the parent.
164
     *
165
     * @param array $items
166
     * @param bool $active should the parent be active too
167
     *
168
     * @return array
169
     *
170
     * {@see items}
171
     */
172 12
    private function isChildActive(array $items, bool &$active): array
173
    {
174 12
        foreach ($items as $i => $child) {
175 11
            if ($this->isItemActive($child)) {
176 3
                ArrayHelper::setValue($items[$i], 'active', true);
177 3
                if ($this->activateParents) {
178 1
                    $active = $this->activateParents;
179
                }
180
            }
181
182 11
            if (is_array($child) && ($childItems = ArrayHelper::getValue($child, 'items')) && is_array($childItems)) {
183 1
                $activeParent = false;
184 1
                $items[$i]['items'] = $this->isChildActive($childItems, $activeParent);
185
186 1
                if ($activeParent) {
187 1
                    $items[$i]['options'] ??= ['class' => ''];
188 1
                    Html::addCssClass($items[$i]['options'], 'active');
189 1
                    $active = $activeParent;
190
                }
191
            }
192
        }
193
194 12
        return $items;
195
    }
196
197
    /**
198
     * Checks whether a menu item is active.
199
     *
200
     * This is done by checking if {@see currentPath} match that specified in the `url` option of the menu item. When
201
     * the `url` option of a menu item is specified in terms of an array, its first element is treated as the
202
     * currentPath for the item and the rest of the elements are the associated parameters. Only when its currentPath
203
     * and parameters match {@see currentPath}, respectively, will a menu item be considered active.
204
     *
205
     * @param array|object|string $item the menu item to be checked
206
     *
207
     * @return bool whether the menu item is active
208
     */
209 18
    private function isItemActive($item): bool
210
    {
211 18
        if (isset($item['active'])) {
212 5
            return ArrayHelper::getValue($item, 'active');
0 ignored issues
show
Bug Best Practice introduced by
The expression return Yiisoft\Arrays\Ar...tValue($item, 'active') could return the type null which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
Bug introduced by
It seems like $item can also be of type string; however, parameter $array of Yiisoft\Arrays\ArrayHelper::getValue() does only seem to accept array|object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

212
            return ArrayHelper::getValue(/** @scrutinizer ignore-type */ $item, 'active');
Loading history...
213
        }
214
215
        return
216 18
            isset($item['url']) &&
217 18
            $this->currentPath !== '/' &&
218 18
            $item['url'] === $this->currentPath &&
219 18
            $this->activateItems;
220
    }
221
222 18
    private function renderIcon(string $label, string $icon, array $iconOptions): string
223
    {
224 18
        if ($icon !== '') {
225 2
            $label = Html::beginTag('span', $iconOptions) .
226 2
                Html::tag('i', '', ['class' => $icon]) .
227 2
                Html::endTag('span') .
228 2
                Html::tag('span', $label);
229
        }
230
231 18
        return $label;
232
    }
233
234
    /**
235
     * Renders a widget's item.
236
     *
237
     * @param array $item the item to render.
238
     *
239
     * @throws InvalidArgumentException|JsonException
240
     *
241
     * @return string the rendering result.
242
     */
243 19
    private function renderItem(array $item): string
244
    {
245 19
        if (!isset($item['label'])) {
246 1
            throw new InvalidArgumentException('The "label" option is required.');
247
        }
248
249 18
        $dropdown = false;
250 18
        $this->encodeLabels = $item['encode'] ?? $this->encodeLabels;
251
252 18
        if ($this->encodeLabels) {
253 17
            $label = Html::encode($item['label']);
254
        } else {
255 3
            $label = $item['label'];
256
        }
257
258 18
        $iconOptions = [];
259
260 18
        $icon = $item['icon'] ?? '';
261
262 18
        if (array_key_exists('iconOptions', $item) && is_array($item['iconOptions'])) {
263 2
            $iconOptions = $this->addOptions($iconOptions, 'icon');
264
        }
265
266 18
        $label = $this->renderIcon($label, $icon, $iconOptions);
267
268 18
        $options = ArrayHelper::getValue($item, 'options', []);
269 18
        $items = ArrayHelper::getValue($item, 'items');
270 18
        $url = ArrayHelper::getValue($item, 'url', '#');
271 18
        $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []);
272 18
        $disabled = ArrayHelper::getValue($item, 'disabled', false);
273
274 18
        $active = $this->isItemActive($item);
275
276 18
        if (isset($items)) {
277 12
            $dropdown = true;
278
279 12
            Html::addCssClass($options, 'navbar-item has-dropdown is-hoverable');
280
281 12
            if (is_array($items)) {
282 12
                $items = $this->isChildActive($items, $active);
283 12
                $items = $this->renderDropdown($items, $item);
284
            }
285
        }
286
287 17
        Html::addCssClass($linkOptions, 'navbar-item');
288
289 17
        if ($disabled) {
290 1
            Html::addCssStyle($linkOptions, 'opacity:.65; pointer-events:none;');
291
        }
292
293
        /** @psalm-suppress ConflictingReferenceConstraint */
294 17
        if ($this->activateItems && $active) {
295 2
            Html::addCssClass($linkOptions, 'is-active');
296
        }
297
298 17
        if ($dropdown) {
299 11
            $dropdownOptions = ['class' => 'navbar-link', 'encode' => false];
300
301
            return
302 11
                Html::beginTag('div', $options) . "\n" .
303 11
                Html::a($label, $url, $dropdownOptions) . "\n" .
304 11
                $items .
305 11
                Html::endTag('div');
306
        }
307
308 13
        if ($this->encodeTags === false) {
309 12
            $linkOptions['encode'] = false;
310
        }
311
312 13
        return Html::a($label, $url, $linkOptions);
313
    }
314
}
315