Completed
Push — master ( d27882...fd8f2b )
by Lars
14s queued 13s
created

HtmlMinDomObserverOptimizeAttributes   F

Complexity

Total Complexity 79

Size/Duplication

Total Lines 301
Duplicated Lines 6.64 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 94.39%

Importance

Changes 0
Metric Value
wmc 79
lcom 1
cbo 3
dl 20
loc 301
ccs 101
cts 107
cp 0.9439
rs 2.08
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A domElementBeforeMinification() 0 3 1
F removeAttributeHelper() 20 71 42
F domElementAfterMinification() 0 102 20
B removeHttpPrefixHelper() 0 37 11
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 50
    public function domElementBeforeMinification(SimpleHtmlDomInterface $element, HtmlMinInterface $htmlMin)
40
    {
41 50
    }
42
43
    /**
44
     * Receive dom elements after the minification.
45
     *
46
     * @param SimpleHtmlDomInterface $element
47
     * @param HtmlMinInterface       $htmlMin
48
     *
49
     * @return void
50
     */
51 50
    public function domElementAfterMinification(SimpleHtmlDomInterface $element, HtmlMinInterface $htmlMin)
52
    {
53 50
		$tag_name = $element->getNode()->nodeName;
54 50
        $attributes = $element->getAllAttributes();
55 50
        if ($attributes === null) {
56 49
            return;
57
        }
58
59 33
        $attrs = [];
60 33
        foreach ((array) $attributes as $attrName => $attrValue) {
61
62
            // -------------------------------------------------------------------------
63
            // Remove optional "http:"-prefix from attributes.
64
            // -------------------------------------------------------------------------
65
66 33
            if ($htmlMin->isDoRemoveHttpPrefixFromAttributes()) {
67 5
                $attrValue = $this->removeHttpPrefixHelper(
68 5
                    $attrValue,
69 5
                    $attrName,
70 5
                    'http',
71 5
					$attributes,
72 5
					$tag_name,
73 5
                    $htmlMin
74
                );
75
            }
76
77 33
            if ($htmlMin->isDoRemoveHttpsPrefixFromAttributes()) {
78 1
                $attrValue = $this->removeHttpPrefixHelper(
79 1
                    $attrValue,
80 1
                    $attrName,
81 1
                    'https',
82 1
					$attributes,
83 1
					$tag_name,
84 1
                    $htmlMin
85
                );
86
            }
87
88 33
            if ($htmlMin->isDoMakeSameDomainLinksRelative()) {
89
90 1
                $localDomain = $htmlMin->getLocalDomain();
91
                /** @noinspection InArrayCanBeUsedInspection */
92
                if (
93
                    (
94 1
                        $attrName === 'href'
95
                        ||
96 1
                        $attrName === 'src'
97
                        ||
98 1
                        $attrName === 'srcset'
99
                        ||
100 1
                        $attrName === 'action'
101
                    )
102
                    &&
103 1
                    !(isset($attributes['rel']) && $attributes['rel'] === 'external')
104
                    &&
105 1
                    !(isset($attributes['target']) && $attributes['target'] === '_blank')
106
                    &&
107 1
                    \stripos($attrValue, $localDomain) !== false
108
                ) {
109 1
                    $localDomainEscaped = \preg_quote($localDomain, '/');
110
111 1
                    $attrValue = (string) \preg_replace("/^(?:(?:https?:)?\/\/)?{$localDomainEscaped}(?!\w)(?:\/?)/i", '/', $attrValue);
112
                }
113
            }
114
115 33
            if ($this->removeAttributeHelper(
116 33
                $element->tag,
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...
117 33
                $attrName,
118 33
                $attrValue,
119 33
                $attributes,
120 33
                $htmlMin
121
            )) {
122 6
                $element->{$attrName} = null;
123
124 6
                continue;
125
            }
126
127
            // -------------------------------------------------------------------------
128
            // Sort css-class-names, for better gzip results.
129
            // -------------------------------------------------------------------------
130
131 33
            if ($htmlMin->isDoSortCssClassNames()) {
132 32
                $attrValue = $this->sortCssClassNames($attrName, $attrValue);
133
            }
134
135 33
            if ($htmlMin->isDoSortHtmlAttributes()) {
136 32
                $attrs[$attrName] = $attrValue;
137 32
                $element->{$attrName} = null;
138
            }
139
        }
140
141
        // -------------------------------------------------------------------------
142
        // Sort html-attributes, for better gzip results.
143
        // -------------------------------------------------------------------------
144
145 33
        if ($htmlMin->isDoSortHtmlAttributes()) {
146 32
            \ksort($attrs);
147 32
            foreach ($attrs as $attrName => $attrValue) {
148 32
                $attrValue = HtmlDomParser::replaceToPreserveHtmlEntities($attrValue);
149 32
                $element->setAttribute((string) $attrName, $attrValue, true);
150
            }
151
        }
152 33
    }
153
154
    /**
155
     * Check if the attribute can be removed.
156
     *
157
     * @param string           $tag
158
     * @param string           $attrName
159
     * @param string           $attrValue
160
     * @param array            $allAttr
161
     * @param HtmlMinInterface $htmlMin
162
     *
163
     * @return bool
164
     */
165 33
    private function removeAttributeHelper($tag, $attrName, $attrValue, $allAttr, HtmlMinInterface $htmlMin): bool
166
    {
167
        // remove defaults
168 33
        if ($htmlMin->isDoRemoveDefaultAttributes()) {
169 1
            if ($tag === 'script' && $attrName === 'language' && $attrValue === 'javascript') {
170
                return true;
171
            }
172
173 1
            if ($tag === 'form' && $attrName === 'method' && $attrValue === 'get') {
174
                return true;
175
            }
176
177 1
            if ($tag === 'input' && $attrName === 'type' && $attrValue === 'text') {
178
                return true;
179
            }
180
181 1
            if ($tag === 'area' && $attrName === 'shape' && $attrValue === 'rect') {
182
                return true;
183
            }
184
        }
185
186
        // remove deprecated charset-attribute (the browser will use the charset from the HTTP-Header, anyway)
187 33
        if ($htmlMin->isDoRemoveDeprecatedScriptCharsetAttribute()) {
188
            /** @noinspection NestedPositiveIfStatementsInspection */
189 32
            if ($tag === 'script' && $attrName === 'charset' && !isset($allAttr['src'])) {
190
                return true;
191
            }
192
        }
193
194
        // remove deprecated anchor-jump
195 33 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...
196
            /** @noinspection NestedPositiveIfStatementsInspection */
197 32
            if ($tag === 'a' && $attrName === 'name' && isset($allAttr['id']) && $allAttr['id'] === $attrValue) {
198
                return true;
199
            }
200
        }
201
202
        // remove "type=text/css" for css links
203 33 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...
204
            /** @noinspection NestedPositiveIfStatementsInspection */
205 32
            if ($tag === 'link' && $attrName === 'type' && $attrValue === 'text/css' && isset($allAttr['rel']) && $allAttr['rel'] === 'stylesheet') {
206 1
                return true;
207
            }
208
        }
209
210
        // remove deprecated script-mime-types
211 33 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...
212
            /** @noinspection NestedPositiveIfStatementsInspection */
213 32
            if ($tag === 'script' && $attrName === 'type' && isset($allAttr['src'], self::$executableScriptsMimeTypes[$attrValue])) {
214 1
                return true;
215
            }
216
        }
217
218
        // remove 'value=""' from <input type="text">
219 33 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...
220
            /** @noinspection NestedPositiveIfStatementsInspection */
221 32
            if ($tag === 'input' && $attrName === 'value' && $attrValue === '' && isset($allAttr['type']) && $allAttr['type'] === 'text') {
222 1
                return true;
223
            }
224
        }
225
226
        // remove some empty attributes
227 33
        if ($htmlMin->isDoRemoveEmptyAttributes()) {
228
            /** @noinspection NestedPositiveIfStatementsInspection */
229 32
            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)) {
230 5
                return true;
231
            }
232
        }
233
234 33
        return false;
235
    }
236
237
    /**
238
     * @param string           $attrValue
239
     * @param string           $attrName
240
     * @param string           $scheme
241
     * @param array            $attributes
242
     * @param HtmlMinInterface $htmlMin
243
     *
244
     * @return string
245
     */
246 5
    private function removeHttpPrefixHelper(
247
        string $attrValue,
248
        string $attrName,
249
        string $scheme,
250
		array $attributes,
251
		string $tag_name,
252
        HtmlMinInterface $htmlMin
253
    ): string {
254
        /** @noinspection InArrayCanBeUsedInspection */
255
        if (
256 5
            !(isset($attributes['rel']) && $attributes['rel'] === 'external')
257
            &&
258 5
            !(isset($attributes['target']) && $attributes['target'] === '_blank')
259
            &&
260
            (
261
                (
262 5
                    $attrName === 'href'
263
                    &&
264
					(
265 4
						!$htmlMin->isKeepPrefixOnExternalAttributes()
266
						||
267 4
						$tag_name === 'link'
268
					)
269
				)
270
                ||
271 5
                $attrName === 'src'
272
                ||
273 5
                $attrName === 'srcset'
274
                ||
275 5
                $attrName === 'action'
276
            )
277
        ) {
278 4
            $attrValue = \str_replace($scheme . '://', '//', $attrValue);
279
        }
280
281 5
        return $attrValue;
282
    }
283
284
    /**
285
     * @param string $attrName
286
     * @param string $attrValue
287
     *
288
     * @return string
289
     */
290 32
    private function sortCssClassNames($attrName, $attrValue): string
291
    {
292 32
        if ($attrName !== 'class' || !$attrValue) {
293 27
            return $attrValue;
294
        }
295
296 18
        $classes = \array_unique(
297 18
            \explode(' ', $attrValue)
298
        );
299 18
        \sort($classes);
300
301 18
        $attrValue = '';
302 18
        foreach ($classes as $class) {
303 18
            if (!$class) {
304 3
                continue;
305
            }
306
307 18
            $attrValue .= \trim($class) . ' ';
308
        }
309
310 18
        return \trim($attrValue);
311
    }
312
}
313