Completed
Push — master ( b3b2b5...2a7a8f )
by Lars
01:23
created

HtmlMinDomObserverOptimizeAttributes   F

Complexity

Total Complexity 79

Size/Duplication

Total Lines 290
Duplicated Lines 6.9 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 93%

Importance

Changes 0
Metric Value
wmc 79
lcom 1
cbo 3
dl 20
loc 290
ccs 93
cts 100
cp 0.93
rs 2.08
c 0
b 0
f 0

5 Methods

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