Completed
Pull Request — master (#47)
by Lars
02:14 queued 43s
created

HtmlMinDomObserverOptimizeAttributes   F

Complexity

Total Complexity 82

Size/Duplication

Total Lines 313
Duplicated Lines 5.75 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 93.75%

Importance

Changes 0
Metric Value
wmc 82
lcom 1
cbo 3
dl 18
loc 313
ccs 90
cts 96
cp 0.9375
rs 2
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A domElementBeforeMinification() 0 3 1
B removeUrlSchemeHelper() 0 37 11
A sortCssClassNames() 0 22 5
F removeAttributeHelper() 18 71 44
F domElementAfterMinification() 0 111 21

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