Completed
Push — master ( d7cd82...c94b19 )
by Mikołaj
04:50
created

Navigation::sortItems()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 13
rs 9.8333
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->isAtribute('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->isAtribute(
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->isAtribute('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->isAtribute('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'
227
                    .'%4$s'.' href="'.'%5$s'.'">%6$s%7$s'.'%8$s'.'%9$s</a>%10$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->isAtribute(
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 title=""
245
                    $this->isAtribute('title', $item['caption']),
246
247
                    # %5$s a href=""
248
                    $item['slug'],
249
250
                    # %6$s ico
251
                    $this->addContainerWithIcoIf(
252
                        $item['ico'],
253
                        $config['li_a_ico-container'],
254
                        $config['li_a_ico-class_base']
255
                    ),
256
257
                    # %7$s before text in li a
258
                    $before['li_a_text'],
259
260
                    # %8$s text inside item
261
                    $this->addContainerWithSelectorIf($item['title'], $config['li_a_text-container']),
262
263
                    # %9$s after text in li a
264
                    $after['li_a_text'],
265
266
                    # %10$s text after li a
267
                    $after['li_a']
268
                );
269
            }
270
        }
271
272
        $html[] = !empty($after['last_root_li']) ? str_repeat("\t", $nesting + 1).$after['last_root_li'] : '';
273
274
        // HTML wrapper for the menu (close)
275
        $html[] = str_repeat("\t", $nesting).'</ul>';
276
277
        $html[] = $after['root_ul'];
278
279
        return implode("\n", array_filter($html))."\n";
280
    }
281
282
    /**
283
     * @return int
284
     */
285
    public function getRootID()
286
    {
287
        return $this->rootID;
288
    }
289
290
    /**
291
     * Set root ID.
292
     *
293
     * @param int $id ID of element to start create tree. Set 0 to create full tree
294
     */
295
    public function setRootID($id)
296
    {
297
        $this->rootID = (int)$id;
298
    }
299
300
    /**
301
     * @param array $items
302
     *
303
     * @return MenuItem[]
304
     */
305
    protected function sortItems(array $items)
306
    {
307
        usort(
308
            $items,
309
            function ($a, $b) {
310
                /** @var MenuItem $a */
311
                /** @var MenuItem $b */
312
                return $a->getPosition() > $b->getPosition();
313
            }
314
        );
315
316
        return $items;
317
    }
318
319
    /**
320
     * @return MenuItem[]
321
     */
322
    public function getItems()
323
    {
324
        return $this->menuItemsCollection->getByType($this->getType());
325
    }
326
327
    /**
328
     * @return string
329
     */
330
    public function getType()
331
    {
332
        return $this->type;
333
    }
334
335
    /**
336
     * Menu type defined in menu_types table.
337
     *
338
     * @param string $type
339
     */
340
    public function setType($type)
341
    {
342
        $this->type = $type;
343
    }
344
345
    /**
346
     * @return array
347
     */
348
    public function getCurrents()
349
    {
350
        $currents = $this->currents;
351
352
        // add actual app dir to currents slug
353
        foreach ($currents as $key => $value) {
354
            $currents[$key] = DIR.'/'.$value;
355
        }
356
357
        return $currents;
358
    }
359
360
    /**
361
     * @return array
362
     */
363
    public function getClasses()
364
    {
365
        return array_merge(
366
            [
367
                'root_ul'       => '',
368
                'li'            => '',
369
                'li_active'     => '',
370
                'li_with_ul'    => '',
371
                'li_without_ul' => '',
372
                'sub_ul'        => '',
373
            ],
374
            $this->classes
375
        );
376
    }
377
378
    /**
379
     * Set classes to use in menu.
380
     *
381
     * @param array $classes
382
     *      'root_ul' (string) Main <ul>
383
     *      `li` (string) Each <li>
384
     *      'li_active' (string)
385
     *      'li_with_ul' (string) <li> with <ul>
386
     *      'li_without_ul' (string) <li> without <ul>
387
     *      'sub_ul' (string) <ul> inside <li>
388
     */
389
    public function setClasses(array $classes)
390
    {
391
        $this->classes = $classes;
392
    }
393
394
    /**
395
     * @return array
396
     */
397 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...
398
    {
399
        return array_merge(
400
            [
401
                'root_ul'           => '',
402
                'first_root_li'     => '',
403
                'li_a'              => '',
404
                'li_a_text'         => '',
405
                'li_with_ul_a'      => '',
406
                'li_with_ul_a_text' => '',
407
            ],
408
            $this->before
409
        );
410
    }
411
412
    /**
413
     * Put string before elements.
414
     *
415
     * @param array $before
416
     *      'root_ul' (string) Main <ul>
417
     *      'first_root_li' (string) First <li> in main <ul>
418
     *      'li_a' (string) In <li> before <a>
419
     *      'li_a_text' (string) In <li><a> before text inside
420
     *      'li_with_ul_a' (string) In <li> with <ul> before <a>
421
     *      'li_with_ul_a_text' (string) In <li><a> with <ul> before text inside
422
     */
423
    public function setBefore(array $before)
424
    {
425
        $this->before = $before;
426
    }
427
428
    /**
429
     * @return array
430
     */
431 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...
432
    {
433
        return array_merge(
434
            [
435
                'root_ul'           => '',
436
                'last_root_li'      => '',
437
                'li_a'              => '',
438
                'li_a_text'         => '',
439
                'li_with_ul_a'      => '',
440
                'li_with_ul_a_text' => '',
441
            ],
442
            $this->after
443
        );
444
    }
445
446
    /**
447
     * Put string after elements.
448
     *
449
     * @param array $after Texts after
450
     *                     'root_ul' (string) Main <ul>
451
     *                     'last_root_li' (string) Last <li> in main <ul>
452
     *                     'li_a' (string) In <li> after <a>
453
     *                     'li_a_text' (string) In <li><a> before text inside
454
     *                     'li_with_ul_a' (string) In <li> with <ul> after <a>
455
     *                     'li_with_ul_a_text' (string) In <li><a> with <ul> after text inside
456
     */
457
    public function setAfter(array $after)
458
    {
459
        $this->after = $after;
460
    }
461
462
    /**
463
     * @return mixed
464
     */
465
    public function getNesting()
466
    {
467
        return $this->nesting;
468
    }
469
470
    /**
471
     * Set generated menu code nesting.
472
     *
473
     * @param int $nesting
474
     */
475
    public function setNesting($nesting)
476
    {
477
        $this->nesting = $nesting;
478
    }
479
480
    /**
481
     * @return array
482
     */
483
    public function getConfig()
484
    {
485
        return array_merge(
486
            [
487
                'li_a_text-container' => '',
488
                'li_a_ico-container'  => '',
489
                'li_a_ico-class_base' => '',
490
            ],
491
            $this->config
492
        );
493
    }
494
495
    /**
496
     * Set config.
497
     *
498
     * @param array $config
499
     *                      'li_a_text-container' (string) Selector container for text in <li><a>
500
     *                      'li_a_ico-container' (string) Selector container for ico in <li><a>
501
     *                      'li_a_ico-class_base' (string) Base class of icon container
502
     */
503
    public function setConfig(array $config)
504
    {
505
        $this->config = $config;
506
    }
507
508
    /**
509
     * Put value is not empty.
510
     *
511
     * @param string       $atribute
512
     * @param string|array $value
513
     *
514
     * @return string
515
     */
516 View Code Duplication
    private function isAtribute($atribute, $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...
517
    {
518
        if (is_array($value)) {
519
            array_filter($value);
520
            $value = trim(implode(' ', $value));
521
522
            return !empty($value) ? ' '.$atribute.'="'.$value.'"' : '';
523
        }
524
525
        return (isset($value) and !empty($value)) ? ' '.$atribute.'="'.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...
526
    }
527
528
    protected function each(&$arr)
529
    {
530
        $key    = key($arr);
531
        $result = ($key === null) ? false : [$key, current($arr), 'key' => $key, 'value' => current($arr)];
532
        next($arr);
533
534
        return $result;
535
    }
536
537
    /**
538
     * Check is item active.
539
     *
540
     * @param string $slug  Current slug
541
     * @param array  $array Active slugs
542
     *
543
     * @return bool
544
     */
545
    private function isActive($slug, $array)
546
    {
547
        return in_array($slug, $array);
548
    }
549
550
    private function addContainerWithIcoIf($ico, $selector, $classBase)
551
    {
552
        if (empty($ico) || empty($selector)) {
553
            return false;
554
        }
555
556
        return '<'.$selector.' class="'.$classBase.' '.$ico.'"></'.$selector.'> ';
557
    }
558
559
    private function addContainerWithSelectorIf($inside, $selector)
560
    {
561
        if (empty($selector)) {
562
            return $inside;
563
        }
564
565
        return '<'.$selector.'>'.$inside.'</'.$selector.'>';
566
    }
567
}
568