Completed
Push — master ( c94b19...263ae6 )
by Mikołaj
03:41
created

Navigation::isAttribute()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 11

Duplication

Lines 11
Ratio 100 %

Importance

Changes 0
Metric Value
cc 5
nc 6
nop 2
dl 11
loc 11
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
namespace Rudolf\Component\Html;
4
5
use Rudolf\Component\Helpers\Navigation\MenuItem;
6
use Rudolf\Component\Helpers\Navigation\MenuItemCollection;
7
8
class Navigation
9
{
10
    /**
11
     * @var int
12
     */
13
    private $rootID = 0;
14
15
    /**
16
     * @var string
17
     */
18
    private $type;
19
20
    /**
21
     * @var MenuItemCollection
22
     */
23
    private $menuItemsCollection;
24
25
    /**
26
     * @var array
27
     */
28
    private $currents = [];
29
30
    /**
31
     * @var array
32
     */
33
    private $classes = [];
34
35
    /**
36
     * @var int
37
     */
38
    private $nesting;
39
40
    /**
41
     * @var array
42
     */
43
    private $before = [];
44
45
    /**
46
     * @var array
47
     */
48
    private $after = [];
49
50
    /**
51
     * @var array
52
     */
53
    private $config = [];
54
55
    /**
56
     * Set items.
57
     *
58
     * @param MenuItemCollection $items
59
     */
60
    public function setItems(MenuItemCollection $items)
61
    {
62
        $this->menuItemsCollection = $items;
63
    }
64
65
    /**
66
     * Set active elements slugs, use to mark current items.
67
     *
68
     * @param array|string $currents
69
     */
70
    public function setCurrent($currents)
71
    {
72
        if (!is_array($currents)) {
73
            $address = explode('/', trim($currents, '/'));
74
75
            $currents = [];
76
            $temp     = '';
77
            foreach ($address as $key => $value) {
78
                $currents[] = ltrim($temp = $temp.'/'.$value, '/');
79
            }
80
        }
81
82
        $this->currents = $currents;
83
    }
84
85
    /**
86
     * Menu creator.
87
     * @link   http://pastebin.com/GAFvSew4
88
     * @author J. Bruni - original author
89
     * @return string|bool
90
     */
91
    public function create()
92
    {
93
        $root_id  = $this->getRootID();
94
        $items    = $this->sortItems($this->getItems());
95
        $currents = $this->getCurrents();
96
        $classes  = $this->getClasses();
97
        $before   = $this->getBefore();
98
        $after    = $this->getAfter();
99
        $nesting  = $this->getNesting();
100
        $config   = $this->getConfig();
101
102
        if (empty($items)) {
103
            return false;
104
        }
105
106 View Code Duplication
        foreach ($items as $item) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
107
            if (null !== $item->getParentId()) {
108
                $children[$item->getParentId()][] = $item;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$children was never initialized. Although not strictly required by PHP, it is generally a good practice to add $children = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
109
            }
110
        }
111
112
        // loop will be false if the root has no children (i.e., an empty menu!)
113
        $loop = !empty($children[$root_id]);
114
115
        // initializing $parent as the root
116
        $parent       = $root_id;
117
        $parent_stack = [];
118
119
        $html = [];
120
121
        $html[] = $before['root_ul'];
122
123
        // HTML wrapper for the menu (open)
124
        $html[] = sprintf(
125
            '%1$s'.'<ul'.'%2$s'.'>',
126
            # %1$s tab if text before
127
            !empty($before['root_ul']) ? str_repeat("\t", $nesting) : '',
128
129
            # %2$s root ul class
130
            $this->isAttribute('class', $classes['root_ul'])
131
        );
132
133
        $html[] = !empty($before['first_root_li']) ? str_repeat("\t", $nesting + 1).$before['first_root_li'] : '';
134
135
        // loop
136
        while ($loop && (($item = $this->each($children[$parent])) || ($parent > $root_id))) {
0 ignored issues
show
Bug introduced by
The variable $children does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
137 View Code Duplication
            if (is_object($item['value'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
138
                /**
139
                 * @var MenuItem $obj
140
                 */
141
                $obj  = $item['value'];
142
                $item = [
143
                    'id'        => $obj->getId(),
144
                    'parent_id' => $obj->getParentId(),
145
                    'title'     => $obj->getTitle(),
146
                    'slug'      => $obj->getSlug(),
147
                    'caption'   => $obj->getCaption(),
148
                    'ico'       => $obj->getIco(),
149
                ];
150
            }
151
152
            // HTML for menu item containing children (close)
153
            if ($item === false) {
154
                $parent = array_pop($parent_stack);
155
                $html[] = str_repeat("\t", (count($parent_stack) + 1) * 2 + $nesting).'</ul>';
156
                $html[] = str_repeat("\t", (count($parent_stack) + 1) * 2 - 1 + $nesting).'</li>';
157
            } // HTML for menu item containing children (open)
158
            elseif (!empty($children[$item['id']])) {
159
                $tab = str_repeat("\t", (count($parent_stack) + 1) * 2 - 1 + $nesting);
160
161
                /*
162
                 * <li> with <ul>
163
                 */
164
                $html[] = sprintf(
165
                    '%1$s'.'<li'.'%2$s'.'>%3$s<a'
166
                    .'%4$s'.' href="'.'%5$s'.'">%6$s%7$s'.'%8$s'.'%9$s</a>%10$s',
167
                    # %1$s tabulation
168
                    $tab,
169
170
                    # %2$s li class (active)
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
171
                    $this->isAttribute(
172
                        'class',
173
                        [
174
                            $classes['li'],
175
                            $classes['li_with_ul'],
176
                            $this->isActive($item['slug'], $currents) ? $classes['li_active'] : '',
177
                        ]
178
                    ),
179
180
                    # %3$s text before li a
181
                    $before['li_with_ul_a'],
182
183
                    # %4$s a title=""
184
                    $this->isAttribute('title', $item['caption']),
185
186
                    # %5$s a href=""
187
                    $item['slug'],
188
189
                    # %6$s ico
190
                    $this->addContainerWithIcoIf(
191
                        $item['ico'],
192
                        $config['li_a_ico-container'],
193
                        $config['li_a_ico-class_base']
194
                    ),
195
196
                    # %7$s before text in li a
197
                    $before['li_with_ul_a_text'],
198
199
                    # %8$s text inside item
200
                    $this->addContainerWithSelectorIf($item['title'], $config['li_a_text-container']),
201
202
                    # %9$s after text in li a
203
                    $after['li_with_ul_a_text'],
204
205
                    # %10$s text after li a
206
                    $after['li_with_ul_a']
207
                );
208
209
                /*
210
                 * sub <ul> in <li>
211
                 */
212
                $html[] = sprintf(
213
                    '%1$s'.'<ul'.'%2$s'.'>',
214
                    # %1$s tabulation
215
                    $tab."\t",
216
217
                    # %2$s sub ul class
218
                    $this->isAttribute('class', $classes['sub_ul'])
219
                );
220
221
                $parent_stack[] = $item['parent_id'];
222
                $parent         = $item['id'];
223
            } // HTML for menu item with no children (aka "leaf")
224
            else {
225
                $html[] = sprintf(
226
                    '%1$s'.'<li'.'%2$s'.'>%3$s<a'.'%4$s'
227
                    .'%5$s'.' href="'.'%6$s'.'">%7$s%8$s'.'%9$s'.'%10$s</a>%11$s',
228
                    # %1$s tabulation
229
                    str_repeat("\t", (count($parent_stack) + 1) * 2 - 1 + $nesting),
230
231
                    # %2$s li class (active)
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
232
                    $this->isAttribute(
233
                        'class',
234
                        [
235
                            $classes['li'],
236
                            $classes['li_without_ul'],
237
                            $this->isActive($item['slug'], $currents) ? $classes['li_active'] : '',
238
                        ]
239
                    ),
240
241
                    # %3$s text before li a
242
                    $before['li_a'],
243
244
                    # %4$s a class=""
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
245
                    $this->isAttribute(
246
                        'class',
247
                        [
248
                            $classes['a'],
249
                            $this->isActive($item['slug'], $currents) ? $classes['a_active'] : '',
250
                        ]
251
                    ),
252
253
                    # %5$s a title=""
254
                    $this->isAttribute('title', $item['caption']),
255
256
                    # %6$s a href=""
257
                    $item['slug'],
258
259
                    # %7$s ico
260
                    $this->addContainerWithIcoIf(
261
                        $item['ico'],
262
                        $config['li_a_ico-container'],
263
                        $config['li_a_ico-class_base']
264
                    ),
265
266
                    # %8$s before text in li a
267
                    $before['li_a_text'],
268
269
                    # %9$s text inside item
270
                    $this->addContainerWithSelectorIf($item['title'], $config['li_a_text-container']),
271
272
                    # %10$s after text in li a
273
                    $after['li_a_text'],
274
275
                    # %11$s text after li a
276
                    $after['li_a']
277
                );
278
            }
279
        }
280
281
        $html[] = !empty($after['last_root_li']) ? str_repeat("\t", $nesting + 1).$after['last_root_li'] : '';
282
283
        // HTML wrapper for the menu (close)
284
        $html[] = str_repeat("\t", $nesting).'</ul>';
285
286
        $html[] = $after['root_ul'];
287
288
        return implode("\n", array_filter($html))."\n";
289
    }
290
291
    /**
292
     * @return int
293
     */
294
    public function getRootID()
295
    {
296
        return $this->rootID;
297
    }
298
299
    /**
300
     * Set root ID.
301
     *
302
     * @param int $id ID of element to start create tree. Set 0 to create full tree
303
     */
304
    public function setRootID($id)
305
    {
306
        $this->rootID = (int)$id;
307
    }
308
309
    /**
310
     * @param array $items
311
     *
312
     * @return MenuItem[]
313
     */
314
    protected function sortItems(array $items)
315
    {
316
        usort(
317
            $items,
318
            function ($a, $b) {
319
                /** @var MenuItem $a */
320
                /** @var MenuItem $b */
321
                return $a->getPosition() > $b->getPosition();
322
            }
323
        );
324
325
        return $items;
326
    }
327
328
    /**
329
     * @return MenuItem[]
330
     */
331
    public function getItems()
332
    {
333
        return $this->menuItemsCollection->getByType($this->getType());
334
    }
335
336
    /**
337
     * @return string
338
     */
339
    public function getType()
340
    {
341
        return $this->type;
342
    }
343
344
    /**
345
     * Menu type defined in menu_types table.
346
     *
347
     * @param string $type
348
     */
349
    public function setType($type)
350
    {
351
        $this->type = $type;
352
    }
353
354
    /**
355
     * @return array
356
     */
357
    public function getCurrents()
358
    {
359
        $currents = $this->currents;
360
361
        // add actual app dir to currents slug
362
        foreach ($currents as $key => $value) {
363
            $currents[$key] = DIR.'/'.$value;
364
        }
365
366
        return $currents;
367
    }
368
369
    /**
370
     * @return array
371
     */
372
    public function getClasses()
373
    {
374
        return array_merge(
375
            [
376
                'root_ul'       => '',
377
                'li'            => '',
378
                'a'             => '',
379
                'li_active'     => '',
380
                'a_active'      => '',
381
                'li_with_ul'    => '',
382
                'li_without_ul' => '',
383
                'sub_ul'        => '',
384
            ],
385
            $this->classes
386
        );
387
    }
388
389
    /**
390
     * Set classes to use in menu.
391
     *
392
     * @param array $classes
393
     *      'root_ul' (string) Main <ul>
394
     *      `li` (string) Each <li>
395
     *      `a` (string) Each <a>
396
     *      'li_active' (string)
397
     *      'a_active' (string)
398
     *      'li_with_ul' (string) <li> with <ul>
399
     *      'li_without_ul' (string) <li> without <ul>
400
     *      'sub_ul' (string) <ul> inside <li>
401
     */
402
    public function setClasses(array $classes)
403
    {
404
        $this->classes = $classes;
405
    }
406
407
    /**
408
     * @return array
409
     */
410 View Code Duplication
    public function getBefore()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
411
    {
412
        return array_merge(
413
            [
414
                'root_ul'           => '',
415
                'first_root_li'     => '',
416
                'li_a'              => '',
417
                'li_a_text'         => '',
418
                'li_with_ul_a'      => '',
419
                'li_with_ul_a_text' => '',
420
            ],
421
            $this->before
422
        );
423
    }
424
425
    /**
426
     * Put string before elements.
427
     *
428
     * @param array $before
429
     *      'root_ul' (string) Main <ul>
430
     *      'first_root_li' (string) First <li> in main <ul>
431
     *      'li_a' (string) In <li> before <a>
432
     *      'li_a_text' (string) In <li><a> before text inside
433
     *      'li_with_ul_a' (string) In <li> with <ul> before <a>
434
     *      'li_with_ul_a_text' (string) In <li><a> with <ul> before text inside
435
     */
436
    public function setBefore(array $before)
437
    {
438
        $this->before = $before;
439
    }
440
441
    /**
442
     * @return array
443
     */
444 View Code Duplication
    public function getAfter()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
445
    {
446
        return array_merge(
447
            [
448
                'root_ul'           => '',
449
                'last_root_li'      => '',
450
                'li_a'              => '',
451
                'li_a_text'         => '',
452
                'li_with_ul_a'      => '',
453
                'li_with_ul_a_text' => '',
454
            ],
455
            $this->after
456
        );
457
    }
458
459
    /**
460
     * Put string after elements.
461
     *
462
     * @param array $after Texts after
463
     *                     'root_ul' (string) Main <ul>
464
     *                     'last_root_li' (string) Last <li> in main <ul>
465
     *                     'li_a' (string) In <li> after <a>
466
     *                     'li_a_text' (string) In <li><a> before text inside
467
     *                     'li_with_ul_a' (string) In <li> with <ul> after <a>
468
     *                     'li_with_ul_a_text' (string) In <li><a> with <ul> after text inside
469
     */
470
    public function setAfter(array $after)
471
    {
472
        $this->after = $after;
473
    }
474
475
    /**
476
     * @return mixed
477
     */
478
    public function getNesting()
479
    {
480
        return $this->nesting;
481
    }
482
483
    /**
484
     * Set generated menu code nesting.
485
     *
486
     * @param int $nesting
487
     */
488
    public function setNesting($nesting)
489
    {
490
        $this->nesting = $nesting;
491
    }
492
493
    /**
494
     * @return array
495
     */
496
    public function getConfig()
497
    {
498
        return array_merge(
499
            [
500
                'li_a_text-container' => '',
501
                'li_a_ico-container'  => '',
502
                'li_a_ico-class_base' => '',
503
            ],
504
            $this->config
505
        );
506
    }
507
508
    /**
509
     * Set config.
510
     *
511
     * @param array $config
512
     *                      'li_a_text-container' (string) Selector container for text in <li><a>
513
     *                      'li_a_ico-container' (string) Selector container for ico in <li><a>
514
     *                      'li_a_ico-class_base' (string) Base class of icon container
515
     */
516
    public function setConfig(array $config)
517
    {
518
        $this->config = $config;
519
    }
520
521
    /**
522
     * Put value is not empty.
523
     *
524
     * @param string       $attribute
525
     * @param string|array $value
526
     *
527
     * @return string
528
     */
529 View Code Duplication
    private function isAttribute($attribute, $value)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
530
    {
531
        if (is_array($value)) {
532
            array_filter($value);
533
            $value = trim(implode(' ', $value));
534
535
            return !empty($value) ? ' '.$attribute.'="'.$value.'"' : '';
536
        }
537
538
        return (isset($value) and !empty($value)) ? ' '.$attribute.'="'.trim($value).'"' : '';
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
539
    }
540
541
    protected function each(&$arr)
542
    {
543
        $key    = key($arr);
544
        $result = ($key === null) ? false : [$key, current($arr), 'key' => $key, 'value' => current($arr)];
545
        next($arr);
546
547
        return $result;
548
    }
549
550
    /**
551
     * Check is item active.
552
     *
553
     * @param string $slug  Current slug
554
     * @param array  $array Active slugs
555
     *
556
     * @return bool
557
     */
558
    private function isActive($slug, $array)
559
    {
560
        return in_array($slug, $array);
561
    }
562
563
    private function addContainerWithIcoIf($ico, $selector, $classBase)
564
    {
565
        if (empty($ico) || empty($selector)) {
566
            return false;
567
        }
568
569
        return '<'.$selector.' class="'.$classBase.' '.$ico.'"></'.$selector.'> ';
570
    }
571
572
    private function addContainerWithSelectorIf($inside, $selector)
573
    {
574
        if (empty($selector)) {
575
            return $inside;
576
        }
577
578
        return '<'.$selector.'>'.$inside.'</'.$selector.'>';
579
    }
580
}
581