Completed
Push — development ( 0937b7...1a7c5e )
by Thomas
21s
created

MainRenderer   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 286
Duplicated Lines 9.09 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 0
Metric Value
dl 26
loc 286
rs 8.3673
c 0
b 0
f 0
wmc 45
lcom 1
cbo 3

10 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 25 1
B render() 0 25 4
A renderList() 0 18 4
B renderChildren() 0 18 5
F renderItem() 0 68 15
A renderLink() 0 10 4
A renderLinkElement() 13 13 2
A renderSpanElement() 13 13 2
A renderLabel() 0 8 3
B format() 0 19 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like MainRenderer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MainRenderer, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Oc\Menu\Renderer;
4
5
use Knp\Menu\ItemInterface;
6
use Knp\Menu\Matcher\MatcherInterface;
7
use Knp\Menu\Renderer\Renderer;
8
use Knp\Menu\Renderer\RendererInterface;
9
10
/**
11
 * Class MainRenderer
12
 *
13
 * @package Oc\Menu\Renderer
14
 */
15
class MainRenderer extends Renderer implements RendererInterface
16
{
17
    protected $matcher;
18
    protected $defaultOptions;
19
20
    /**
21
     * @param MatcherInterface $matcher
22
     * @param array            $defaultOptions
23
     * @param string           $charset
24
     */
25
    public function __construct(MatcherInterface $matcher, array $defaultOptions = array(), $charset = null)
26
    {
27
        $this->matcher = $matcher;
28
        $this->defaultOptions = array_merge(array(
29
            'depth' => null,
30
            'matchingDepth' => null,
31
            'currentAsLink' => true,
32
            'currentClass' => 'navigation-item--current',
33
            'ancestorClass' => null,
34
            'listClass' => 'navigation',
35
            'listLevelPrefixClass' => 'navigation--level-',
36
            'itemClass' => 'navigation__item navigation-item',
37
            'textClass' => 'navigation-item__text',
38
            'linkClass' => 'navigation-item__link',
39
            'firstClass' => 'navigation-item--first',
40
            'lastClass' => 'navigation-item--last',
41
            'compressed' => false,
42
            'allow_safe_labels' => false,
43
            'clear_matcher' => true,
44
            'leaf_class' => null,
45
            'branch_class' => null,
46
        ), $defaultOptions);
47
48
        parent::__construct($charset);
49
    }
50
51
    public function render(ItemInterface $item, array $options = array())
52
    {
53
        $options = array_merge($this->defaultOptions, $options);
54
55
        $childrenClass = (array) $item->getChildrenAttribute('class');
56
57
        if ($options['listClass']) {
58
            $childrenClass[] = $options['listClass'];
59
        }
60
61
        if ($options['listLevelPrefixClass']) {
62
            $childrenClass[] = $options['listLevelPrefixClass'] . $item->getLevel();
63
        }
64
65
        $childrenAttributes = $item->getChildrenAttributes();
66
        $childrenAttributes['class'] = implode(' ', $childrenClass);
67
68
        $html = $this->renderList($item, $childrenAttributes, $options);
69
70
        if ($options['clear_matcher']) {
71
            $this->matcher->clear();
72
        }
73
74
        return $html;
75
    }
76
77
    protected function renderList(ItemInterface $item, array $attributes, array $options)
78
    {
79
        /**
80
         * Return an empty string if any of the following are true:
81
         *   a) The menu has no children eligible to be displayed
82
         *   b) The depth is 0
83
         *   c) This menu item has been explicitly set to hide its children
84
         */
85
        if (!$item->hasChildren() || 0 === $options['depth'] || !$item->getDisplayChildren()) {
86
            return '';
87
        }
88
89
        $html = $this->format('<ul'.$this->renderHtmlAttributes($attributes).'>', 'ul', $item->getLevel(), $options);
90
        $html .= $this->renderChildren($item, $options);
91
        $html .= $this->format('</ul>', 'ul', $item->getLevel(), $options);
92
93
        return $html;
94
    }
95
96
    /**
97
     * Renders all of the children of this menu.
98
     *
99
     * This calls ->renderItem() on each menu item, which instructs each
100
     * menu item to render themselves as an <li> tag (with nested ul if it
101
     * has children).
102
     * This method updates the depth for the children.
103
     *
104
     * @param ItemInterface $item
105
     * @param array         $options The options to render the item.
106
     *
107
     * @return string
108
     */
109
    protected function renderChildren(ItemInterface $item, array $options)
110
    {
111
        // render children with a depth - 1
112
        if (null !== $options['depth']) {
113
            $options['depth'] = $options['depth'] - 1;
114
        }
115
116
        if (null !== $options['matchingDepth'] && $options['matchingDepth'] > 0) {
117
            $options['matchingDepth'] = $options['matchingDepth'] - 1;
118
        }
119
120
        $html = '';
121
        foreach ($item->getChildren() as $child) {
122
            $html .= $this->renderItem($child, $options);
123
        }
124
125
        return $html;
126
    }
127
128
    /**
129
     * Called by the parent menu item to render this menu.
130
     *
131
     * This renders the li tag to fit into the parent ul as well as its
132
     * own nested ul tag if this menu item has children
133
     *
134
     * @param ItemInterface $item
135
     * @param array         $options The options to render the item
136
     *
137
     * @return string
138
     */
139
    protected function renderItem(ItemInterface $item, array $options)
140
    {
141
        // if we don't have access or this item is marked to not be shown
142
        if (!$item->isDisplayed()) {
143
            return '';
144
        }
145
146
        // create an array than can be imploded as a class list
147
        $class = (array) $item->getAttribute('class');
148
149
        if ($options['itemClass']) {
150
            $class[] = $options['itemClass'];
151
        }
152
153
        if ($this->matcher->isCurrent($item)) {
154
            $class[] = $options['currentClass'];
155
        } elseif ($this->matcher->isAncestor($item, $options['matchingDepth'])) {
156
            $class[] = $options['ancestorClass'];
157
        }
158
159
        if ($item->actsLikeFirst()) {
160
            $class[] = $options['firstClass'];
161
        }
162
        if ($item->actsLikeLast()) {
163
            $class[] = $options['lastClass'];
164
        }
165
166
        if ($item->hasChildren() && $options['depth'] !== 0) {
167
            if (null !== $options['branch_class'] && $item->getDisplayChildren()) {
168
                $class[] = $options['branch_class'];
169
            }
170
        } elseif (null !== $options['leaf_class']) {
171
            $class[] = $options['leaf_class'];
172
        }
173
174
        // retrieve the attributes and put the final class string back on it
175
        $attributes = $item->getAttributes();
176
        if (!empty($class)) {
177
            $attributes['class'] = implode(' ', $class);
178
        }
179
180
        // opening li tag
181
        $html = $this->format('<li'.$this->renderHtmlAttributes($attributes).'>', 'li', $item->getLevel(), $options);
182
183
        // render the text/link inside the li tag
184
        //$html .= $this->format($item->getUri() ? $item->renderLink() : $item->renderLabel(), 'link', $item->getLevel());
0 ignored issues
show
Unused Code Comprehensibility introduced by
65% 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...
185
        $html .= $this->renderLink($item, $options);
186
187
        // renders the embedded ul
188
        $childrenClass = (array) $item->getChildrenAttribute('class');
189
        if ($options['listClass']) {
190
            $childrenClass[] = $options['listClass'];
191
        }
192
193
        if ($options['listLevelPrefixClass']) {
194
            $childrenClass[] = $options['listLevelPrefixClass'] . $item->getLevel();
195
        }
196
197
        $childrenAttributes = $item->getChildrenAttributes();
198
        $childrenAttributes['class'] = implode(' ', $childrenClass);
199
200
        $html .= $this->renderList($item, $childrenAttributes, $options);
201
202
        // closing li tag
203
        $html .= $this->format('</li>', 'li', $item->getLevel(), $options);
204
205
        return $html;
206
    }
207
208
    /**
209
     * Renders the link in a a tag with link attributes or
210
     * the label in a span tag with label attributes
211
     *
212
     * Tests if item has a an uri and if not tests if it's
213
     * the current item and if the text has to be rendered
214
     * as a link or not.
215
     *
216
     * @param ItemInterface $item    The item to render the link or label for
217
     * @param array         $options The options to render the item
218
     *
219
     * @return string
220
     */
221
    protected function renderLink(ItemInterface $item, array $options = array())
222
    {
223
        if ($item->getUri() && (!$item->isCurrent() || $options['currentAsLink'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $item->isCurrent() of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
224
            $text = $this->renderLinkElement($item, $options);
225
        } else {
226
            $text = $this->renderSpanElement($item, $options);
227
        }
228
229
        return $this->format($text, 'link', $item->getLevel(), $options);
230
    }
231
232 View Code Duplication
    protected function renderLinkElement(ItemInterface $item, array $options)
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...
233
    {
234
        $childrenClass = (array) $item->getChildrenAttribute('class');
235
236
        if ($options['linkClass']) {
237
            $childrenClass[] = $options['linkClass'];
238
        }
239
240
        $childrenAttributes = $item->getChildrenAttributes();
241
        $childrenAttributes['class'] = implode(' ', $childrenClass);
242
243
        return sprintf('<a href="%s"%s>%s</a>', $this->escape($item->getUri()), $this->renderHtmlAttributes($childrenAttributes), $this->renderLabel($item, $options));
244
    }
245
246 View Code Duplication
    protected function renderSpanElement(ItemInterface $item, array $options)
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...
247
    {
248
        $childrenClass = (array) $item->getChildrenAttribute('class');
249
250
        if ($options['textClass']) {
251
            $childrenClass[] = $options['textClass'];
252
        }
253
254
        $childrenAttributes = $item->getChildrenAttributes();
255
        $childrenAttributes['class'] = implode(' ', $childrenClass);
256
257
        return sprintf('<span%s>%s</span>', $this->renderHtmlAttributes($childrenAttributes), $this->renderLabel($item, $options));
258
    }
259
260
    protected function renderLabel(ItemInterface $item, array $options)
261
    {
262
        if ($options['allow_safe_labels'] && $item->getExtra('safe_label', false)) {
263
            return $item->getLabel();
264
        }
265
266
        return $this->escape($item->getLabel());
267
    }
268
269
    /**
270
     * If $this->renderCompressed is on, this will apply the necessary
271
     * spacing and line-breaking so that the particular thing being rendered
272
     * makes up its part in a fully-rendered and spaced menu.
273
     *
274
     * @param string  $html    The html to render in an (un)formatted way
275
     * @param string  $type    The type [ul,link,li] of thing being rendered
276
     * @param integer $level
277
     * @param array   $options
278
     *
279
     * @return string
280
     */
281
    protected function format($html, $type, $level, array $options)
282
    {
283
        if ($options['compressed']) {
284
            return $html;
285
        }
286
287
        switch ($type) {
288
            case 'ul':
289
            case 'link':
290
                $spacing = $level * 4;
291
                break;
292
293
            case 'li':
294
                $spacing = $level * 4 - 2;
295
                break;
296
        }
297
298
        return str_repeat(' ', $spacing).$html."\n";
0 ignored issues
show
Bug introduced by
The variable $spacing 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...
299
    }
300
}
301