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

domElementAfterMinification()   F

Complexity

Conditions 21
Paths 304

Size

Total Lines 96

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 46
CRAP Score 21.0042

Importance

Changes 0
Metric Value
dl 0
loc 96
ccs 46
cts 47
cp 0.9787
rs 2.0333
c 0
b 0
f 0
cc 21
nc 304
nop 2
crap 21.0042

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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