Completed
Push — master ( 61ae49...b42d4b )
by Lars
11:14 queued 10:00
created

HtmlMinDomObserverOptimizeAttributes   F

Complexity

Total Complexity 67

Size/Duplication

Total Lines 244
Duplicated Lines 8.2 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 92.21%

Importance

Changes 0
Metric Value
wmc 67
lcom 1
cbo 3
dl 20
loc 244
ccs 71
cts 77
cp 0.9221
rs 3.04
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A domElementBeforeMinification() 0 3 1
C domElementAfterMinification() 0 64 10
F removeAttributeHelper() 20 71 42
B removeHttpPrefixHelper() 0 19 9
A sortCssClassNames() 0 22 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 HtmlMinDomObserverOptimizeAttributes 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 HtmlMinDomObserverOptimizeAttributes, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\helper;
6
7
/**
8
 * HtmlMinDomObserverOptimizeAttributes: Optimize html attributes. [protected html is still protected]
9
 *
10
 * Sort HTML-Attributes, so that gzip can do better work and remove some default attributes...
11
 */
12
final class HtmlMinDomObserverOptimizeAttributes implements HtmlMinDomObserverInterface
13
{
14
    /**
15
     * // https://mathiasbynens.be/demo/javascript-mime-type
16
     * // https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
17
     *
18
     * @var string[]
19
     *
20
     * @psalm-var array<string, string>
21
     */
22
    private static $executableScriptsMimeTypes = [
23
        'text/javascript'          => '',
24
        'text/ecmascript'          => '',
25
        'text/jscript'             => '',
26
        'application/javascript'   => '',
27
        'application/x-javascript' => '',
28
        'application/ecmascript'   => '',
29
    ];
30
31
    /**
32
     * Receive dom elements before the minification.
33
     *
34
     * @param SimpleHtmlDomInterface $element
35
     * @param HtmlMinInterface       $htmlMin
36
     *
37
     * @return void
38
     */
39 48
    public function domElementBeforeMinification(SimpleHtmlDomInterface $element, HtmlMinInterface $htmlMin)
40
    {
41 48
    }
42
43
    /**
44
     * Receive dom elements after the minification.
45
     *
46
     * @param SimpleHtmlDomInterface $element
47
     * @param HtmlMinInterface       $htmlMin
48
     *
49
     * @return void
50
     */
51 48
    public function domElementAfterMinification(SimpleHtmlDomInterface $element, HtmlMinInterface $htmlMin)
52
    {
53 48
        $attributes = $element->getAllAttributes();
54 48
        if ($attributes === null) {
55 47
            return;
56
        }
57
58 31
        $attrs = [];
59 31
        foreach ((array) $attributes as $attrName => $attrValue) {
60
61
            // -------------------------------------------------------------------------
62
            // Remove optional "http:"-prefix from attributes.
63
            // -------------------------------------------------------------------------
64
65 31
            if ($htmlMin->isDoRemoveHttpPrefixFromAttributes()) {
66 30
                $attrValue = $this->removeHttpPrefixHelper(
67 30
                    $attrValue,
68
                    $attrName,
69 30
                    'http',
70
                    $attributes
71
                );
72
            }
73
74 31
            if ($htmlMin->isDoRemoveHttpsPrefixFromAttributes()) {
75 1
                $attrValue = $this->removeHttpPrefixHelper(
76 1
                    $attrValue,
77
                    $attrName,
78 1
                    'https',
79
                    $attributes
80
                );
81
            }
82
83 31
            if ($this->removeAttributeHelper($element->tag, $attrName, $attrValue, $attributes, $htmlMin)) {
0 ignored issues
show
Bug introduced by
Accessing tag on the interface voku\helper\SimpleHtmlDomInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
84 6
                $element->{$attrName} = null;
85
86 6
                continue;
87
            }
88
89
            // -------------------------------------------------------------------------
90
            // Sort css-class-names, for better gzip results.
91
            // -------------------------------------------------------------------------
92
93 31
            if ($htmlMin->isDoSortCssClassNames()) {
94 30
                $attrValue = $this->sortCssClassNames($attrName, $attrValue);
95
            }
96
97 31
            if ($htmlMin->isDoSortHtmlAttributes()) {
98 30
                $attrs[$attrName] = $attrValue;
99 30
                $element->{$attrName} = null;
100
            }
101
        }
102
103
        // -------------------------------------------------------------------------
104
        // Sort html-attributes, for better gzip results.
105
        // -------------------------------------------------------------------------
106
107 31
        if ($htmlMin->isDoSortHtmlAttributes()) {
108 30
            \ksort($attrs);
109 30
            foreach ($attrs as $attrName => $attrValue) {
110 30
                $attrValue = HtmlDomParser::replaceToPreserveHtmlEntities($attrValue);
111 30
                $element->setAttribute((string) $attrName, $attrValue, true);
112
            }
113
        }
114 31
    }
115
116
    /**
117
     * Check if the attribute can be removed.
118
     *
119
     * @param string  $tag
120
     * @param string  $attrName
121
     * @param string  $attrValue
122
     * @param array   $allAttr
123
     * @param HtmlMinInterface $htmlMin
124
     *
125
     * @return bool
126
     */
127 31
    private function removeAttributeHelper($tag, $attrName, $attrValue, $allAttr, HtmlMinInterface $htmlMin): bool
128
    {
129
        // remove defaults
130 31
        if ($htmlMin->isDoRemoveDefaultAttributes()) {
131 1
            if ($tag === 'script' && $attrName === 'language' && $attrValue === 'javascript') {
132
                return true;
133
            }
134
135 1
            if ($tag === 'form' && $attrName === 'method' && $attrValue === 'get') {
136
                return true;
137
            }
138
139 1
            if ($tag === 'input' && $attrName === 'type' && $attrValue === 'text') {
140
                return true;
141
            }
142
143 1
            if ($tag === 'area' && $attrName === 'shape' && $attrValue === 'rect') {
144
                return true;
145
            }
146
        }
147
148
        // remove deprecated charset-attribute (the browser will use the charset from the HTTP-Header, anyway)
149 31
        if ($htmlMin->isDoRemoveDeprecatedScriptCharsetAttribute()) {
150
            /** @noinspection NestedPositiveIfStatementsInspection */
151 30
            if ($tag === 'script' && $attrName === 'charset' && !isset($allAttr['src'])) {
152
                return true;
153
            }
154
        }
155
156
        // remove deprecated anchor-jump
157 31 View Code Duplication
        if ($htmlMin->isDoRemoveDeprecatedAnchorName()) {
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...
158
            /** @noinspection NestedPositiveIfStatementsInspection */
159 30
            if ($tag === 'a' && $attrName === 'name' && isset($allAttr['id']) && $allAttr['id'] === $attrValue) {
160
                return true;
161
            }
162
        }
163
164
        // remove "type=text/css" for css links
165 31 View Code Duplication
        if ($htmlMin->isDoRemoveDeprecatedTypeFromStylesheetLink()) {
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...
166
            /** @noinspection NestedPositiveIfStatementsInspection */
167 30
            if ($tag === 'link' && $attrName === 'type' && $attrValue === 'text/css' && isset($allAttr['rel']) && $allAttr['rel'] === 'stylesheet') {
168 1
                return true;
169
            }
170
        }
171
172
        // remove deprecated script-mime-types
173 31 View Code Duplication
        if ($htmlMin->isDoRemoveDeprecatedTypeFromScriptTag()) {
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...
174
            /** @noinspection NestedPositiveIfStatementsInspection */
175 30
            if ($tag === 'script' && $attrName === 'type' && isset($allAttr['src'], self::$executableScriptsMimeTypes[$attrValue])) {
176 1
                return true;
177
            }
178
        }
179
180
        // remove 'value=""' from <input type="text">
181 31 View Code Duplication
        if ($htmlMin->isDoRemoveValueFromEmptyInput()) {
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...
182
            /** @noinspection NestedPositiveIfStatementsInspection */
183 30
            if ($tag === 'input' && $attrName === 'value' && $attrValue === '' && isset($allAttr['type']) && $allAttr['type'] === 'text') {
184 1
                return true;
185
            }
186
        }
187
188
        // remove some empty attributes
189 31
        if ($htmlMin->isDoRemoveEmptyAttributes()) {
190
            /** @noinspection NestedPositiveIfStatementsInspection */
191 30
            if (\trim($attrValue) === '' && \preg_match('/^(?:class|id|style|title|lang|dir|on(?:focus|blur|change|click|dblclick|mouse(?:down|up|over|move|out)|key(?:press|down|up)))$/', $attrName)) {
192 5
                return true;
193
            }
194
        }
195
196 31
        return false;
197
    }
198
199
    /**
200
     * @param string $attrValue
201
     * @param string $attrName
202
     * @param string $scheme
203
     * @param array  $attributes
204
     *
205
     * @return string
206
     */
207 30
    private function removeHttpPrefixHelper(
208
        string $attrValue,
209
        string $attrName,
210
        string $scheme,
211
        array $attributes
212
    ): string {
213
        /** @noinspection InArrayCanBeUsedInspection */
214
        if (
215 30
            ($attrName === 'href' || $attrName === 'src' || $attrName === 'srcset' || $attrName === 'action')
216
            &&
217 30
            !(isset($attributes['rel']) && $attributes['rel'] === 'external')
218
            &&
219 30
            !(isset($attributes['target']) && $attributes['target'] === '_blank')
220
        ) {
221 8
            $attrValue = \str_replace($scheme . '://', '//', $attrValue);
222
        }
223
224 30
        return $attrValue;
225
    }
226
227
    /**
228
     * @param string $attrName
229
     * @param string $attrValue
230
     *
231
     * @return string
232
     */
233 30
    private function sortCssClassNames($attrName, $attrValue): string
234
    {
235 30
        if ($attrName !== 'class' || !$attrValue) {
236 25
            return $attrValue;
237
        }
238
239 18
        $classes = \array_unique(
240 18
            \explode(' ', $attrValue)
241
        );
242 18
        \sort($classes);
243
244 18
        $attrValue = '';
245 18
        foreach ($classes as $class) {
246 18
            if (!$class) {
247 3
                continue;
248
            }
249
250 18
            $attrValue .= \trim($class) . ' ';
251
        }
252
253 18
        return \trim($attrValue);
254
    }
255
}
256