Completed
Push — master ( 24ed29...12288c )
by Lars
06:22
created

HtmlMin::domNodeToString()   F

Complexity

Conditions 31
Paths 55

Size

Total Lines 112

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 49
CRAP Score 31.4135

Importance

Changes 0
Metric Value
dl 0
loc 112
ccs 49
cts 53
cp 0.9245
rs 3.3333
c 0
b 0
f 0
cc 31
nc 55
nop 1
crap 31.4135

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
 * Class HtmlMin
9
 *
10
 * Inspired by:
11
 * - JS: https://github.com/kangax/html-minifier/blob/gh-pages/src/htmlminifier.js
12
 * - PHP: https://github.com/searchturbine/phpwee-php-minifier
13
 * - PHP: https://github.com/WyriHaximus/HtmlCompress
14
 * - PHP: https://github.com/zaininnari/html-minifier
15
 * - PHP: https://github.com/ampaze/PHP-HTML-Minifier
16
 * - Java: https://code.google.com/archive/p/htmlcompressor/
17
 *
18
 * Ideas:
19
 * - http://perfectionkills.com/optimizing-html/
20
 */
21
class HtmlMin implements HtmlMinInterface
22
{
23
    /**
24
     * @var string
25
     */
26
    private static $regExSpace = "/[[:space:]]{2,}|[\r\n]/u";
27
28
    /**
29
     * @var array
30
     */
31
    private static $optional_end_tags = [
32
        'html',
33
        'head',
34
        'body',
35
    ];
36
37
    private static $selfClosingTags = [
38
        'area',
39
        'base',
40
        'basefont',
41
        'br',
42
        'col',
43
        'command',
44
        'embed',
45
        'frame',
46
        'hr',
47
        'img',
48
        'input',
49
        'isindex',
50
        'keygen',
51
        'link',
52
        'meta',
53
        'param',
54
        'source',
55
        'track',
56
        'wbr',
57
    ];
58
59
    private static $trimWhitespaceFromTags = [
60
        'article' => '',
61
        'br'      => '',
62
        'div'     => '',
63
        'footer'  => '',
64
        'hr'      => '',
65
        'nav'     => '',
66
        'p'       => '',
67
        'script'  => '',
68
    ];
69
70
    /**
71
     * @var array
72
     */
73
    private static $booleanAttributes = [
74
        'allowfullscreen' => '',
75
        'async'           => '',
76
        'autofocus'       => '',
77
        'autoplay'        => '',
78
        'checked'         => '',
79
        'compact'         => '',
80
        'controls'        => '',
81
        'declare'         => '',
82
        'default'         => '',
83
        'defaultchecked'  => '',
84
        'defaultmuted'    => '',
85
        'defaultselected' => '',
86
        'defer'           => '',
87
        'disabled'        => '',
88
        'enabled'         => '',
89
        'formnovalidate'  => '',
90
        'hidden'          => '',
91
        'indeterminate'   => '',
92
        'inert'           => '',
93
        'ismap'           => '',
94
        'itemscope'       => '',
95
        'loop'            => '',
96
        'multiple'        => '',
97
        'muted'           => '',
98
        'nohref'          => '',
99
        'noresize'        => '',
100
        'noshade'         => '',
101
        'novalidate'      => '',
102
        'nowrap'          => '',
103
        'open'            => '',
104
        'pauseonexit'     => '',
105
        'readonly'        => '',
106
        'required'        => '',
107
        'reversed'        => '',
108
        'scoped'          => '',
109
        'seamless'        => '',
110
        'selected'        => '',
111
        'sortable'        => '',
112
        'truespeed'       => '',
113
        'typemustmatch'   => '',
114
        'visible'         => '',
115
    ];
116
117
    /**
118
     * @var array
119
     */
120
    private static $skipTagsForRemoveWhitespace = [
121
        'code',
122
        'pre',
123
        'script',
124
        'style',
125
        'textarea',
126
    ];
127
128
    /**
129
     * @var array
130
     */
131
    private $protectedChildNodes = [];
132
133
    /**
134
     * @var string
135
     */
136
    private $protectedChildNodesHelper = 'html-min--voku--saved-content';
137
138
    /**
139
     * @var bool
140
     */
141
    private $doOptimizeViaHtmlDomParser = true;
142
143
    /**
144
     * @var bool
145
     */
146
    private $doOptimizeAttributes = true;
147
148
    /**
149
     * @var bool
150
     */
151
    private $doRemoveComments = true;
152
153
    /**
154
     * @var bool
155
     */
156
    private $doRemoveWhitespaceAroundTags = false;
157
158
    /**
159
     * @var bool
160
     */
161
    private $doRemoveOmittedQuotes = true;
162
163
    /**
164
     * @var bool
165
     */
166
    private $doRemoveOmittedHtmlTags = true;
167
168
    /**
169
     * @var bool
170
     */
171
    private $doRemoveHttpPrefixFromAttributes = false;
172
173
    /**
174
     * @var array
175
     */
176
    private $domainsToRemoveHttpPrefixFromAttributes = [
177
        'google.com',
178
        'google.de',
179
    ];
180
181
    /**
182
     * @var bool
183
     */
184
    private $doSortCssClassNames = true;
185
186
    /**
187
     * @var bool
188
     */
189
    private $doSortHtmlAttributes = true;
190
191
    /**
192
     * @var bool
193
     */
194
    private $doRemoveDeprecatedScriptCharsetAttribute = true;
195
196
    /**
197
     * @var bool
198
     */
199
    private $doRemoveDefaultAttributes = false;
200
201
    /**
202
     * @var bool
203
     */
204
    private $doRemoveDeprecatedAnchorName = true;
205
206
    /**
207
     * @var bool
208
     */
209
    private $doRemoveDeprecatedTypeFromStylesheetLink = true;
210
211
    /**
212
     * @var bool
213
     */
214
    private $doRemoveDeprecatedTypeFromScriptTag = true;
215
216
    /**
217
     * @var bool
218
     */
219
    private $doRemoveValueFromEmptyInput = true;
220
221
    /**
222
     * @var bool
223
     */
224
    private $doRemoveEmptyAttributes = true;
225
226
    /**
227
     * @var bool
228
     */
229
    private $doSumUpWhitespace = true;
230
231
    /**
232
     * @var bool
233
     */
234
    private $doRemoveSpacesBetweenTags = false;
235
236
    /**
237
     * @var bool
238
     */
239
    private $keepBrokenHtml = false;
240
241
    /**
242
     * @var bool
243
     */
244
    private $withDocType = false;
245
246
    /**
247
     * @var HtmlMinDomObserverInterface[]|\SplObjectStorage
248
     */
249
    private $domLoopObservers;
250
251
    /**
252
     * @var int
253
     */
254
    private $protected_tags_counter = 0;
255
256
    /**
257
     * HtmlMin constructor.
258
     */
259 52
    public function __construct()
260
    {
261 52
        $this->domLoopObservers = new \SplObjectStorage();
262
263 52
        $this->attachObserverToTheDomLoop(new HtmlMinDomObserverOptimizeAttributes());
264 52
    }
265
266
    /**
267
     * @param HtmlMinDomObserverInterface $observer
268
     *
269
     * @return void
270
     */
271 52
    public function attachObserverToTheDomLoop(HtmlMinDomObserverInterface $observer)
272
    {
273 52
        $this->domLoopObservers->attach($observer);
274 52
    }
275
276
    /**
277
     * @param bool $doOptimizeAttributes
278
     *
279
     * @return $this
280
     */
281 2
    public function doOptimizeAttributes(bool $doOptimizeAttributes = true): self
282
    {
283 2
        $this->doOptimizeAttributes = $doOptimizeAttributes;
284
285 2
        return $this;
286
    }
287
288
    /**
289
     * @param bool $doOptimizeViaHtmlDomParser
290
     *
291
     * @return $this
292
     */
293 1
    public function doOptimizeViaHtmlDomParser(bool $doOptimizeViaHtmlDomParser = true): self
294
    {
295 1
        $this->doOptimizeViaHtmlDomParser = $doOptimizeViaHtmlDomParser;
296
297 1
        return $this;
298
    }
299
300
    /**
301
     * @param bool $doRemoveComments
302
     *
303
     * @return $this
304
     */
305 3
    public function doRemoveComments(bool $doRemoveComments = true): self
306
    {
307 3
        $this->doRemoveComments = $doRemoveComments;
308
309 3
        return $this;
310
    }
311
312
    /**
313
     * @param bool $doRemoveDefaultAttributes
314
     *
315
     * @return $this
316
     */
317 2
    public function doRemoveDefaultAttributes(bool $doRemoveDefaultAttributes = true): self
318
    {
319 2
        $this->doRemoveDefaultAttributes = $doRemoveDefaultAttributes;
320
321 2
        return $this;
322
    }
323
324
    /**
325
     * @param bool $doRemoveDeprecatedAnchorName
326
     *
327
     * @return $this
328
     */
329 2
    public function doRemoveDeprecatedAnchorName(bool $doRemoveDeprecatedAnchorName = true): self
330
    {
331 2
        $this->doRemoveDeprecatedAnchorName = $doRemoveDeprecatedAnchorName;
332
333 2
        return $this;
334
    }
335
336
    /**
337
     * @param bool $doRemoveDeprecatedScriptCharsetAttribute
338
     *
339
     * @return $this
340
     */
341 2
    public function doRemoveDeprecatedScriptCharsetAttribute(bool $doRemoveDeprecatedScriptCharsetAttribute = true): self
342
    {
343 2
        $this->doRemoveDeprecatedScriptCharsetAttribute = $doRemoveDeprecatedScriptCharsetAttribute;
344
345 2
        return $this;
346
    }
347
348
    /**
349
     * @param bool $doRemoveDeprecatedTypeFromScriptTag
350
     *
351
     * @return $this
352
     */
353 2
    public function doRemoveDeprecatedTypeFromScriptTag(bool $doRemoveDeprecatedTypeFromScriptTag = true): self
354
    {
355 2
        $this->doRemoveDeprecatedTypeFromScriptTag = $doRemoveDeprecatedTypeFromScriptTag;
356
357 2
        return $this;
358
    }
359
360
    /**
361
     * @param bool $doRemoveDeprecatedTypeFromStylesheetLink
362
     *
363
     * @return $this
364
     */
365 2
    public function doRemoveDeprecatedTypeFromStylesheetLink(bool $doRemoveDeprecatedTypeFromStylesheetLink = true): self
366
    {
367 2
        $this->doRemoveDeprecatedTypeFromStylesheetLink = $doRemoveDeprecatedTypeFromStylesheetLink;
368
369 2
        return $this;
370
    }
371
372
    /**
373
     * @param bool $doRemoveEmptyAttributes
374
     *
375
     * @return $this
376
     */
377 2
    public function doRemoveEmptyAttributes(bool $doRemoveEmptyAttributes = true): self
378
    {
379 2
        $this->doRemoveEmptyAttributes = $doRemoveEmptyAttributes;
380
381 2
        return $this;
382
    }
383
384
    /**
385
     * @param bool $doRemoveHttpPrefixFromAttributes
386
     *
387
     * @return $this
388
     */
389 4
    public function doRemoveHttpPrefixFromAttributes(bool $doRemoveHttpPrefixFromAttributes = true): self
390
    {
391 4
        $this->doRemoveHttpPrefixFromAttributes = $doRemoveHttpPrefixFromAttributes;
392
393 4
        return $this;
394
    }
395
396
    /**
397
     * @param bool $doRemoveOmittedHtmlTags
398
     *
399
     * @return $this
400
     */
401 1
    public function doRemoveOmittedHtmlTags(bool $doRemoveOmittedHtmlTags = true): self
402
    {
403 1
        $this->doRemoveOmittedHtmlTags = $doRemoveOmittedHtmlTags;
404
405 1
        return $this;
406
    }
407
408
    /**
409
     * @param bool $doRemoveOmittedQuotes
410
     *
411
     * @return $this
412
     */
413 1
    public function doRemoveOmittedQuotes(bool $doRemoveOmittedQuotes = true): self
414
    {
415 1
        $this->doRemoveOmittedQuotes = $doRemoveOmittedQuotes;
416
417 1
        return $this;
418
    }
419
420
    /**
421
     * @param bool $doRemoveSpacesBetweenTags
422
     *
423
     * @return $this
424
     */
425 1
    public function doRemoveSpacesBetweenTags(bool $doRemoveSpacesBetweenTags = true): self
426
    {
427 1
        $this->doRemoveSpacesBetweenTags = $doRemoveSpacesBetweenTags;
428
429 1
        return $this;
430
    }
431
432
    /**
433
     * @param bool $doRemoveValueFromEmptyInput
434
     *
435
     * @return $this
436
     */
437 2
    public function doRemoveValueFromEmptyInput(bool $doRemoveValueFromEmptyInput = true): self
438
    {
439 2
        $this->doRemoveValueFromEmptyInput = $doRemoveValueFromEmptyInput;
440
441 2
        return $this;
442
    }
443
444
    /**
445
     * @param bool $doRemoveWhitespaceAroundTags
446
     *
447
     * @return $this
448
     */
449 5
    public function doRemoveWhitespaceAroundTags(bool $doRemoveWhitespaceAroundTags = true): self
450
    {
451 5
        $this->doRemoveWhitespaceAroundTags = $doRemoveWhitespaceAroundTags;
452
453 5
        return $this;
454
    }
455
456
    /**
457
     * @param bool $doSortCssClassNames
458
     *
459
     * @return $this
460
     */
461 2
    public function doSortCssClassNames(bool $doSortCssClassNames = true): self
462
    {
463 2
        $this->doSortCssClassNames = $doSortCssClassNames;
464
465 2
        return $this;
466
    }
467
468
    /**
469
     * @param bool $doSortHtmlAttributes
470
     *
471
     * @return $this
472
     */
473 2
    public function doSortHtmlAttributes(bool $doSortHtmlAttributes = true): self
474
    {
475 2
        $this->doSortHtmlAttributes = $doSortHtmlAttributes;
476
477 2
        return $this;
478
    }
479
480
    /**
481
     * @param bool $doSumUpWhitespace
482
     *
483
     * @return $this
484
     */
485 2
    public function doSumUpWhitespace(bool $doSumUpWhitespace = true): self
486
    {
487 2
        $this->doSumUpWhitespace = $doSumUpWhitespace;
488
489 2
        return $this;
490
    }
491
492 48
    private function domNodeAttributesToString(\DOMNode $node): string
493
    {
494
        // Remove quotes around attribute values, when allowed (<p class="foo"> → <p class=foo>)
495 48
        $attr_str = '';
496 48
        if ($node->attributes !== null) {
497 48
            foreach ($node->attributes as $attribute) {
498 31
                $attr_str .= $attribute->name;
499
500
                if (
501 31
                    $this->doOptimizeAttributes
502
                    &&
503 31
                    isset(self::$booleanAttributes[$attribute->name])
504
                ) {
505 9
                    $attr_str .= ' ';
506
507 9
                    continue;
508
                }
509
510 31
                $attr_str .= '=';
511
512
                // http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#attributes-0
513 31
                $omit_quotes = $this->doRemoveOmittedQuotes
514
                               &&
515 31
                               $attribute->value !== ''
516
                               &&
517 31
                               \strpos($attribute->name, '____SIMPLE_HTML_DOM__VOKU') !== 0
518
                               &&
519 31
                               \strpos($attribute->name, ' ') === false
520
                               &&
521 31
                               \preg_match('/["\'=<>` \t\r\n\f]/', $attribute->value) === 0;
522
523 31
                $quoteTmp = '"';
524
                if (
525 31
                    !$omit_quotes
526
                    &&
527 31
                    \strpos($attribute->value, '"') !== false
528
                ) {
529 1
                    $quoteTmp = "'";
530
                }
531
532
                if (
533 31
                    $this->doOptimizeAttributes
534
                    &&
535
                    (
536 30
                        $attribute->name === 'srcset'
537
                        ||
538 31
                        $attribute->name === 'sizes'
539
                    )
540
                ) {
541 2
                    $attr_val = \preg_replace(self::$regExSpace, ' ', $attribute->value);
542
                } else {
543 31
                    $attr_val = $attribute->value;
544
                }
545
546 31
                $attr_str .= ($omit_quotes ? '' : $quoteTmp) . $attr_val . ($omit_quotes ? '' : $quoteTmp);
547 31
                $attr_str .= ' ';
548
            }
549
        }
550
551 48
        return \trim($attr_str);
552
    }
553
554
    /**
555
     * @param \DOMNode $node
556
     *
557
     * @return bool
558
     */
559 47
    private function domNodeClosingTagOptional(\DOMNode $node): bool
560
    {
561 47
        $tag_name = $node->nodeName;
562
563
        /** @var \DOMNode|null $parent_node - false-positive error from phpstan */
564 47
        $parent_node = $node->parentNode;
565
566 47
        if ($parent_node) {
567 47
            $parent_tag_name = $parent_node->nodeName;
568
        } else {
569
            $parent_tag_name = null;
570
        }
571
572 47
        $nextSibling = $this->getNextSiblingOfTypeDOMElement($node);
573
574
        // https://html.spec.whatwg.org/multipage/syntax.html#syntax-tag-omission
575
576
        // Implemented:
577
        //
578
        // A <p> element's end tag may be omitted if the p element is immediately followed by an address, article, aside, blockquote, details, div, dl, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, hr, main, menu, nav, ol, p, pre, section, table, or ul element, or if there is no more content in the parent element and the parent element is an HTML element that is not an a, audio, del, ins, map, noscript, or video element, or an autonomous custom element.
579
        // An <li> element's end tag may be omitted if the li element is immediately followed by another li element or if there is no more content in the parent element.
580
        // A <td> element's end tag may be omitted if the td element is immediately followed by a td or th element, or if there is no more content in the parent element.
581
        // An <option> element's end tag may be omitted if the option element is immediately followed by another option element, or if it is immediately followed by an optgroup element, or if there is no more content in the parent element.
582
        // A <tr> element's end tag may be omitted if the tr element is immediately followed by another tr element, or if there is no more content in the parent element.
583
        // A <th> element's end tag may be omitted if the th element is immediately followed by a td or th element, or if there is no more content in the parent element.
584
        // A <dt> element's end tag may be omitted if the dt element is immediately followed by another dt element or a dd element.
585
        // A <dd> element's end tag may be omitted if the dd element is immediately followed by another dd element or a dt element, or if there is no more content in the parent element.
586
        // An <rp> element's end tag may be omitted if the rp element is immediately followed by an rt or rp element, or if there is no more content in the parent element.
587
588
        /**
589
         * @noinspection TodoComment
590
         *
591
         * TODO: Not Implemented
592
         */
593
        //
594
        // <html> may be omitted if first thing inside is not comment
595
        // <head> may be omitted if first thing inside is an element
596
        // <body> may be omitted if first thing inside is not space, comment, <meta>, <link>, <script>, <style> or <template>
597
        // <colgroup> may be omitted if first thing inside is <col>
598
        // <tbody> may be omitted if first thing inside is <tr>
599
        // An <optgroup> element's end tag may be omitted if the optgroup element is immediately followed by another optgroup element, or if there is no more content in the parent element.
600
        // A <colgroup> element's start tag may be omitted if the first thing inside the colgroup element is a col element, and if the element is not immediately preceded by another colgroup element whose end tag has been omitted. (It can't be omitted if the element is empty.)
601
        // A <colgroup> element's end tag may be omitted if the colgroup element is not immediately followed by ASCII whitespace or a comment.
602
        // A <caption> element's end tag may be omitted if the caption element is not immediately followed by ASCII whitespace or a comment.
603
        // A <thead> element's end tag may be omitted if the thead element is immediately followed by a tbody or tfoot element.
604
        // A <tbody> element's start tag may be omitted if the first thing inside the tbody element is a tr element, and if the element is not immediately preceded by a tbody, thead, or tfoot element whose end tag has been omitted. (It can't be omitted if the element is empty.)
605
        // A <tbody> element's end tag may be omitted if the tbody element is immediately followed by a tbody or tfoot element, or if there is no more content in the parent element.
606
        // A <tfoot> element's end tag may be omitted if there is no more content in the parent element.
607
        //
608
        // <-- However, a start tag must never be omitted if it has any attributes.
609
610 47
        return \in_array($tag_name, self::$optional_end_tags, true)
611
               ||
612
               (
613 44
                   $tag_name === 'li'
614
                   &&
615
                   (
616 6
                       $nextSibling === null
617
                       ||
618
                       (
619 4
                           $nextSibling instanceof \DOMElement
620
                           &&
621 44
                           $nextSibling->tagName === 'li'
622
                       )
623
                   )
624
               )
625
               ||
626
               (
627 44
                   $tag_name === 'rp'
628
                   &&
629
                   (
630
                       $nextSibling === null
631
                       ||
632
                       (
633
                           $nextSibling instanceof \DOMElement
634
                           &&
635
                           (
636
                               $nextSibling->tagName === 'rp'
637
                               ||
638 44
                               $nextSibling->tagName === 'rt'
639
                           )
640
                       )
641
                   )
642
               )
643
               ||
644
               (
645 44
                   $tag_name === 'tr'
646
                   &&
647
                   (
648 1
                       $nextSibling === null
649
                       ||
650
                       (
651 1
                           $nextSibling instanceof \DOMElement
652
                           &&
653 44
                           $nextSibling->tagName === 'tr'
654
                       )
655
                   )
656
               )
657
               ||
658
               (
659 44
                   $tag_name === 'source'
660
                   &&
661
                   (
662 1
                       $parent_tag_name === 'audio'
663
                       ||
664 1
                       $parent_tag_name === 'video'
665
                       ||
666 1
                       $parent_tag_name === 'picture'
667
                       ||
668 44
                       $parent_tag_name === 'source'
669
                   )
670
                   &&
671
                   (
672 1
                       $nextSibling === null
673
                       ||
674
                       (
675
                           $nextSibling instanceof \DOMElement
676
                           &&
677 44
                           $nextSibling->tagName === 'source'
678
                       )
679
                   )
680
               )
681
               ||
682
               (
683
                   (
684 44
                       $tag_name === 'td'
685
                       ||
686 44
                       $tag_name === 'th'
687
                   )
688
                   &&
689
                   (
690 1
                       $nextSibling === null
691
                       ||
692
                       (
693 1
                           $nextSibling instanceof \DOMElement
694
                           &&
695
                           (
696 1
                               $nextSibling->tagName === 'td'
697
                               ||
698 44
                               $nextSibling->tagName === 'th'
699
                           )
700
                       )
701
                   )
702
               )
703
               ||
704
               (
705
                   (
706 44
                       $tag_name === 'dd'
707
                       ||
708 44
                       $tag_name === 'dt'
709
                   )
710
                   &&
711
                   (
712
                       (
713 3
                           $nextSibling === null
714
                           &&
715 3
                           $tag_name === 'dd'
716
                       )
717
                       ||
718
                       (
719 3
                           $nextSibling instanceof \DOMElement
720
                           &&
721
                           (
722 3
                               $nextSibling->tagName === 'dd'
723
                               ||
724 44
                               $nextSibling->tagName === 'dt'
725
                           )
726
                       )
727
                   )
728
               )
729
               ||
730
               (
731 44
                   $tag_name === 'option'
732
                   &&
733
                   (
734 1
                       $nextSibling === null
735
                       ||
736
                       (
737 1
                           $nextSibling instanceof \DOMElement
738
                           &&
739
                           (
740 1
                               $nextSibling->tagName === 'option'
741
                               ||
742 44
                               $nextSibling->tagName === 'optgroup'
743
                           )
744
                       )
745
                   )
746
               )
747
               ||
748
               (
749 44
                   $tag_name === 'p'
750
                   &&
751
                   (
752
                       (
753 14
                           $nextSibling === null
754
                           &&
755
                           (
756 12
                               $node->parentNode !== null
757
                               &&
758
                               !\in_array(
759 12
                                   $node->parentNode->nodeName,
760
                                   [
761
                                       'a',
762
                                       'audio',
763
                                       'del',
764
                                       'ins',
765
                                       'map',
766
                                       'noscript',
767
                                       'video',
768
                                   ],
769
                                   true
770
                               )
771
                           )
772
                       )
773
                       ||
774
                       (
775 9
                           $nextSibling instanceof \DOMElement
776
                           &&
777
                           \in_array(
778 47
                               $nextSibling->tagName,
779
                               [
780
                                   'address',
781
                                   'article',
782
                                   'aside',
783
                                   'blockquote',
784
                                   'dir',
785
                                   'div',
786
                                   'dl',
787
                                   'fieldset',
788
                                   'footer',
789
                                   'form',
790
                                   'h1',
791
                                   'h2',
792
                                   'h3',
793
                                   'h4',
794
                                   'h5',
795
                                   'h6',
796
                                   'header',
797
                                   'hgroup',
798
                                   'hr',
799
                                   'menu',
800
                                   'nav',
801
                                   'ol',
802
                                   'p',
803
                                   'pre',
804
                                   'section',
805
                                   'table',
806
                                   'ul',
807
                               ],
808
                               true
809
                           )
810
                       )
811
                   )
812
               );
813
    }
814
815 48
    protected function domNodeToString(\DOMNode $node): string
816
    {
817
        // init
818 48
        $html = '';
819 48
        $emptyStringTmp = '';
820
821 48
        foreach ($node->childNodes as $child) {
822 48
            if ($emptyStringTmp === 'is_empty') {
823 27
                $emptyStringTmp = 'last_was_empty';
824
            } else {
825 48
                $emptyStringTmp = '';
826
            }
827
828 48
            if ($child instanceof \DOMDocumentType) {
829
                // add the doc-type only if it wasn't generated by DomDocument
830 12
                if (!$this->withDocType) {
831
                    continue;
832
                }
833
834 12
                if ($child->name) {
835 12
                    if (!$child->publicId && $child->systemId) {
836
                        $tmpTypeSystem = 'SYSTEM';
837
                        $tmpTypePublic = '';
838
                    } else {
839 12
                        $tmpTypeSystem = '';
840 12
                        $tmpTypePublic = 'PUBLIC';
841
                    }
842
843 12
                    $html .= '<!DOCTYPE ' . $child->name . ''
844 12
                             . ($child->publicId ? ' ' . $tmpTypePublic . ' "' . $child->publicId . '"' : '')
845 12
                             . ($child->systemId ? ' ' . $tmpTypeSystem . ' "' . $child->systemId . '"' : '')
846 12
                             . '>';
847
                }
848 48
            } elseif ($child instanceof \DOMElement) {
849 48
                $html .= \rtrim('<' . $child->tagName . ' ' . $this->domNodeAttributesToString($child));
850 48
                $html .= '>' . $this->domNodeToString($child);
851
852
                if (
853 48
                    !$this->doRemoveOmittedHtmlTags
854
                    ||
855 48
                    !$this->domNodeClosingTagOptional($child)
856
                ) {
857 42
                    $html .= '</' . $child->tagName . '>';
858
                }
859
860 48
                if (!$this->doRemoveWhitespaceAroundTags) {
861
                    /** @noinspection NestedPositiveIfStatementsInspection */
862
                    if (
863 47
                        $child->nextSibling instanceof \DOMText
864
                        &&
865 47
                        $child->nextSibling->wholeText === ' '
866
                    ) {
867
                        if (
868 26
                            $emptyStringTmp !== 'last_was_empty'
869
                            &&
870 26
                            \substr($html, -1) !== ' '
871
                        ) {
872 26
                            $html = \rtrim($html);
873
874
                            if (
875 26
                                $child->parentNode
876
                                &&
877 26
                                $child->parentNode->nodeName !== 'head'
878
                            ) {
879 26
                                $html .= ' ';
880
                            }
881
                        }
882 48
                        $emptyStringTmp = 'is_empty';
883
                    }
884
                }
885 44
            } elseif ($child instanceof \DOMText) {
886 44
                if ($child->isElementContentWhitespace()) {
887
                    if (
888 30
                        $child->previousSibling !== null
889
                        &&
890 30
                        $child->nextSibling !== null
891
                    ) {
892
                        if (
893
                            (
894 21
                                $child->wholeText
895
                                &&
896 21
                                strpos($child->wholeText, ' ') !== false
897
                            )
898
                            ||
899
                            (
900
                                $emptyStringTmp !== 'last_was_empty'
901
                                &&
902 21
                                \substr($html, -1) !== ' '
903
                            )
904
                        ) {
905 21
                            $html = \rtrim($html);
906
907
                            if (
908 21
                                $child->parentNode
909
                                &&
910 21
                                $child->parentNode->nodeName !== 'head'
911
                            ) {
912 21
                                $html .= ' ';
913
                            }
914
                        }
915 30
                        $emptyStringTmp = 'is_empty';
916
                    }
917
                } else {
918 44
                    $html .= $child->wholeText;
919
                }
920 1
            } elseif ($child instanceof \DOMComment) {
921 1
                $html .= '<!--' . $child->textContent . '-->';
922
            }
923
        }
924
925 48
        return $html;
926
    }
927
928
    /**
929
     * @return array
930
     */
931
    public function getDomainsToRemoveHttpPrefixFromAttributes(): array
932
    {
933
        return $this->domainsToRemoveHttpPrefixFromAttributes;
934
    }
935
936
    /**
937
     * @return bool
938
     */
939
    public function isDoOptimizeAttributes(): bool
940
    {
941
        return $this->doOptimizeAttributes;
942
    }
943
944
    /**
945
     * @return bool
946
     */
947
    public function isDoOptimizeViaHtmlDomParser(): bool
948
    {
949
        return $this->doOptimizeViaHtmlDomParser;
950
    }
951
952
    /**
953
     * @return bool
954
     */
955
    public function isDoRemoveComments(): bool
956
    {
957
        return $this->doRemoveComments;
958
    }
959
960
    /**
961
     * @return bool
962
     */
963 31
    public function isDoRemoveDefaultAttributes(): bool
964
    {
965 31
        return $this->doRemoveDefaultAttributes;
966
    }
967
968
    /**
969
     * @return bool
970
     */
971 31
    public function isDoRemoveDeprecatedAnchorName(): bool
972
    {
973 31
        return $this->doRemoveDeprecatedAnchorName;
974
    }
975
976
    /**
977
     * @return bool
978
     */
979 31
    public function isDoRemoveDeprecatedScriptCharsetAttribute(): bool
980
    {
981 31
        return $this->doRemoveDeprecatedScriptCharsetAttribute;
982
    }
983
984
    /**
985
     * @return bool
986
     */
987 31
    public function isDoRemoveDeprecatedTypeFromScriptTag(): bool
988
    {
989 31
        return $this->doRemoveDeprecatedTypeFromScriptTag;
990
    }
991
992
    /**
993
     * @return bool
994
     */
995 31
    public function isDoRemoveDeprecatedTypeFromStylesheetLink(): bool
996
    {
997 31
        return $this->doRemoveDeprecatedTypeFromStylesheetLink;
998
    }
999
1000
    /**
1001
     * @return bool
1002
     */
1003 31
    public function isDoRemoveEmptyAttributes(): bool
1004
    {
1005 31
        return $this->doRemoveEmptyAttributes;
1006
    }
1007
1008
    /**
1009
     * @return bool
1010
     */
1011 31
    public function isDoRemoveHttpPrefixFromAttributes(): bool
1012
    {
1013 31
        return $this->doRemoveHttpPrefixFromAttributes;
1014
    }
1015
1016
    /**
1017
     * @return bool
1018
     */
1019
    public function isDoRemoveOmittedHtmlTags(): bool
1020
    {
1021
        return $this->doRemoveOmittedHtmlTags;
1022
    }
1023
1024
    /**
1025
     * @return bool
1026
     */
1027
    public function isDoRemoveOmittedQuotes(): bool
1028
    {
1029
        return $this->doRemoveOmittedQuotes;
1030
    }
1031
1032
    /**
1033
     * @return bool
1034
     */
1035
    public function isDoRemoveSpacesBetweenTags(): bool
1036
    {
1037
        return $this->doRemoveSpacesBetweenTags;
1038
    }
1039
1040
    /**
1041
     * @return bool
1042
     */
1043 31
    public function isDoRemoveValueFromEmptyInput(): bool
1044
    {
1045 31
        return $this->doRemoveValueFromEmptyInput;
1046
    }
1047
1048
    /**
1049
     * @return bool
1050
     */
1051
    public function isDoRemoveWhitespaceAroundTags(): bool
1052
    {
1053
        return $this->doRemoveWhitespaceAroundTags;
1054
    }
1055
1056
    /**
1057
     * @return bool
1058
     */
1059 31
    public function isDoSortCssClassNames(): bool
1060
    {
1061 31
        return $this->doSortCssClassNames;
1062
    }
1063
1064
    /**
1065
     * @return bool
1066
     */
1067 31
    public function isDoSortHtmlAttributes(): bool
1068
    {
1069 31
        return $this->doSortHtmlAttributes;
1070
    }
1071
1072
    /**
1073
     * @return bool
1074
     */
1075
    public function isDoSumUpWhitespace(): bool
1076
    {
1077
        return $this->doSumUpWhitespace;
1078
    }
1079
1080
    /**
1081
     * @param string $html
1082
     * @param bool   $multiDecodeNewHtmlEntity
1083
     *
1084
     * @return string
1085
     */
1086 52
    public function minify($html, $multiDecodeNewHtmlEntity = false): string
1087
    {
1088 52
        $html = (string) $html;
1089 52
        if (!isset($html[0])) {
1090 1
            return '';
1091
        }
1092
1093 52
        $html = \trim($html);
1094 52
        if (!$html) {
1095 3
            return '';
1096
        }
1097
1098
        // reset
1099 49
        $this->protectedChildNodes = [];
1100
1101
        // save old content
1102 49
        $origHtml = $html;
1103 49
        $origHtmlLength = \strlen($html);
1104
1105
        // -------------------------------------------------------------------------
1106
        // Minify the HTML via "HtmlDomParser"
1107
        // -------------------------------------------------------------------------
1108
1109 49
        if ($this->doOptimizeViaHtmlDomParser) {
1110 48
            $html = $this->minifyHtmlDom($html, $multiDecodeNewHtmlEntity);
1111
        }
1112
1113
        // -------------------------------------------------------------------------
1114
        // Trim whitespace from html-string. [protected html is still protected]
1115
        // -------------------------------------------------------------------------
1116
1117
        // Remove extra white-space(s) between HTML attribute(s)
1118 49
        if (\strpos($html, ' ') !== false) {
1119 43
            $html = (string) \preg_replace_callback(
1120 43
                '#<([^/\s<>!]+)(?:\s+([^<>]*?)\s*|\s*)(/?)>#',
1121
                static function ($matches) {
1122 43
                    return '<' . $matches[1] . \preg_replace('#([^\s=]+)(=([\'"]?)(.*?)\3)?(\s+|$)#su', ' $1$2', $matches[2]) . $matches[3] . '>';
1123 43
                },
1124 43
                $html
1125
            );
1126
        }
1127
1128 49
        if ($this->doRemoveSpacesBetweenTags) {
1129
            /** @noinspection NestedPositiveIfStatementsInspection */
1130 1
            if (\strpos($html, ' ') !== false) {
1131
                // Remove spaces that are between > and <
1132 1
                $html = (string) \preg_replace('#(>)\s(<)#', '>$2', $html);
1133
            }
1134
        }
1135
1136
        // -------------------------------------------------------------------------
1137
        // Restore protected HTML-code.
1138
        // -------------------------------------------------------------------------
1139
1140 49
        if (\strpos($html, $this->protectedChildNodesHelper) !== false) {
1141 9
            $html = (string) \preg_replace_callback(
1142 9
                '/<(?<element>' . $this->protectedChildNodesHelper . ')(?<attributes> [^>]*)?>(?<value>.*?)<\/' . $this->protectedChildNodesHelper . '>/',
1143 9
                [$this, 'restoreProtectedHtml'],
1144 9
                $html
1145
            );
1146
        }
1147
1148
        // -------------------------------------------------------------------------
1149
        // Restore protected HTML-entities.
1150
        // -------------------------------------------------------------------------
1151
1152 49
        if ($this->doOptimizeViaHtmlDomParser) {
1153 48
            $html = HtmlDomParser::putReplacedBackToPreserveHtmlEntities($html);
1154
        }
1155
1156
        // ------------------------------------
1157
        // Final clean-up
1158
        // ------------------------------------
1159
1160 49
        $html = \str_replace(
1161
            [
1162 49
                'html>' . "\n",
1163
                "\n" . '<html',
1164
                'html/>' . "\n",
1165
                "\n" . '</html',
1166
                'head>' . "\n",
1167
                "\n" . '<head',
1168
                'head/>' . "\n",
1169
                "\n" . '</head',
1170
            ],
1171
            [
1172 49
                'html>',
1173
                '<html',
1174
                'html/>',
1175
                '</html',
1176
                'head>',
1177
                '<head',
1178
                'head/>',
1179
                '</head',
1180
            ],
1181 49
            $html
1182
        );
1183
1184
        // self closing tags, don't need a trailing slash ...
1185 49
        $replace = [];
1186 49
        $replacement = [];
1187 49
        foreach (self::$selfClosingTags as $selfClosingTag) {
1188 49
            $replace[] = '<' . $selfClosingTag . '/>';
1189 49
            $replacement[] = '<' . $selfClosingTag . '>';
1190 49
            $replace[] = '<' . $selfClosingTag . ' />';
1191 49
            $replacement[] = '<' . $selfClosingTag . '>';
1192 49
            $replace[] = '></' . $selfClosingTag . '>';
1193 49
            $replacement[] = '>';
1194
        }
1195 49
        $html = \str_replace(
1196 49
            $replace,
1197 49
            $replacement,
1198 49
            $html
1199
        );
1200
1201
        // ------------------------------------
1202
        // check if compression worked
1203
        // ------------------------------------
1204
1205 49
        if ($origHtmlLength < \strlen($html)) {
1206
            $html = $origHtml;
1207
        }
1208
1209 49
        return $html;
1210
    }
1211
1212
    /**
1213
     * @param \DOMNode $node
1214
     *
1215
     * @return \DOMNode|null
1216
     */
1217 47
    protected function getNextSiblingOfTypeDOMElement(\DOMNode $node)
1218
    {
1219
        do {
1220
            /** @var \DOMNode|null $node - false-positive error from phpstan */
1221 47
            $node = $node->nextSibling;
1222 47
        } while (!($node === null || $node instanceof \DOMElement));
1223
1224 47
        return $node;
1225
    }
1226
1227
    /**
1228
     * Check if the current string is an conditional comment.
1229
     *
1230
     * INFO: since IE >= 10 conditional comment are not working anymore
1231
     *
1232
     * <!--[if expression]> HTML <![endif]-->
1233
     * <![if expression]> HTML <![endif]>
1234
     *
1235
     * @param string $comment
1236
     *
1237
     * @return bool
1238
     */
1239 4
    private function isConditionalComment($comment): bool
1240
    {
1241 4 View Code Duplication
        if (\strpos($comment, '[if ') !== false) {
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...
1242
            /** @noinspection RegExpRedundantEscape */
1243 2
            if (\preg_match('/^\[if [^\]]+\]/', $comment)) {
1244 2
                return true;
1245
            }
1246
        }
1247
1248 4 View Code Duplication
        if (\strpos($comment, '[endif]') !== false) {
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...
1249
            /** @noinspection RegExpRedundantEscape */
1250 1
            if (\preg_match('/\[endif\]$/', $comment)) {
1251 1
                return true;
1252
            }
1253
        }
1254
1255 4
        return false;
1256
    }
1257
1258
    /**
1259
     * @param string $html
1260
     * @param bool   $multiDecodeNewHtmlEntity
1261
     *
1262
     * @return string
1263
     */
1264 48
    private function minifyHtmlDom($html, $multiDecodeNewHtmlEntity): string
1265
    {
1266
        // init dom
1267 48
        $dom = new HtmlDomParser();
1268
        /** @noinspection UnusedFunctionResultInspection */
1269 48
        $dom->useKeepBrokenHtml($this->keepBrokenHtml);
1270
1271 48
        $dom->getDocument()->preserveWhiteSpace = false; // remove redundant white space
1272 48
        $dom->getDocument()->formatOutput = false; // do not formats output with indentation
1273
1274
        // load dom
1275
        /** @noinspection UnusedFunctionResultInspection */
1276 48
        $dom->loadHtml($html);
1277
1278 48
        $this->withDocType = (\stripos(\ltrim($html), '<!DOCTYPE') === 0);
1279
1280
        // -------------------------------------------------------------------------
1281
        // Protect <nocompress> HTML tags first.
1282
        // -------------------------------------------------------------------------
1283
1284 48
        $dom = $this->protectTagHelper($dom, 'nocompress');
1285
1286
        // -------------------------------------------------------------------------
1287
        // Notify the Observer before the minification.
1288
        // -------------------------------------------------------------------------
1289
1290 48
        foreach ($dom->find('*') as $element) {
1291 48
            $this->notifyObserversAboutDomElementBeforeMinification($element);
1292
        }
1293
1294
        // -------------------------------------------------------------------------
1295
        // Protect HTML tags and conditional comments.
1296
        // -------------------------------------------------------------------------
1297
1298 48
        $dom = $this->protectTags($dom);
1299
1300
        // -------------------------------------------------------------------------
1301
        // Remove default HTML comments. [protected html is still protected]
1302
        // -------------------------------------------------------------------------
1303
1304 48
        if ($this->doRemoveComments) {
1305 46
            $dom = $this->removeComments($dom);
1306
        }
1307
1308
        // -------------------------------------------------------------------------
1309
        // Sum-Up extra whitespace from the Dom. [protected html is still protected]
1310
        // -------------------------------------------------------------------------
1311
1312 48
        if ($this->doSumUpWhitespace) {
1313 47
            $dom = $this->sumUpWhitespace($dom);
1314
        }
1315
1316 48
        foreach ($dom->find('*') as $element) {
1317
1318
            // -------------------------------------------------------------------------
1319
            // Remove whitespace around tags. [protected html is still protected]
1320
            // -------------------------------------------------------------------------
1321
1322 48
            if ($this->doRemoveWhitespaceAroundTags) {
1323 3
                $this->removeWhitespaceAroundTags($element);
1324
            }
1325
1326
            // -------------------------------------------------------------------------
1327
            // Notify the Observer after the minification.
1328
            // -------------------------------------------------------------------------
1329
1330 48
            $this->notifyObserversAboutDomElementAfterMinification($element);
1331
        }
1332
1333
        // -------------------------------------------------------------------------
1334
        // Convert the Dom into a string.
1335
        // -------------------------------------------------------------------------
1336
1337 48
        return $dom->fixHtmlOutput(
1338 48
            $this->domNodeToString($dom->getDocument()),
1339 48
            $multiDecodeNewHtmlEntity
1340
        );
1341
    }
1342
1343
    /**
1344
     * @param SimpleHtmlDomInterface $domElement
1345
     *
1346
     * @return void
1347
     */
1348 48
    private function notifyObserversAboutDomElementAfterMinification(SimpleHtmlDomInterface $domElement)
1349
    {
1350 48
        foreach ($this->domLoopObservers as $observer) {
1351 48
            $observer->domElementAfterMinification($domElement, $this);
1352
        }
1353 48
    }
1354
1355
    /**
1356
     * @param SimpleHtmlDomInterface $domElement
1357
     *
1358
     * @return void
1359
     */
1360 48
    private function notifyObserversAboutDomElementBeforeMinification(SimpleHtmlDomInterface $domElement)
1361
    {
1362 48
        foreach ($this->domLoopObservers as $observer) {
1363 48
            $observer->domElementBeforeMinification($domElement, $this);
1364
        }
1365 48
    }
1366
1367
    /**
1368
     * @param HtmlDomParser $dom
1369
     * @param string        $selector
1370
     *
1371
     * @return HtmlDomParser
1372
     */
1373 48
    private function protectTagHelper(HtmlDomParser $dom, string $selector): HtmlDomParser
1374
    {
1375 48
        foreach ($dom->find($selector) as $element) {
1376 5
            if ($element->isRemoved()) {
1377 1
                continue;
1378
            }
1379
1380 5
            $this->protectedChildNodes[$this->protected_tags_counter] = $element->parentNode()->innerHtml();
1381 5
            $element->getNode()->parentNode->nodeValue = '<' . $this->protectedChildNodesHelper . ' data-' . $this->protectedChildNodesHelper . '="' . $this->protected_tags_counter . '"></' . $this->protectedChildNodesHelper . '>';
1382
1383 5
            ++$this->protected_tags_counter;
1384
        }
1385
1386 48
        return $dom;
1387
    }
1388
1389
    /**
1390
     * Prevent changes of inline "styles" and "scripts".
1391
     *
1392
     * @param HtmlDomParser $dom
1393
     *
1394
     * @return HtmlDomParser
1395
     */
1396 48
    private function protectTags(HtmlDomParser $dom): HtmlDomParser
1397
    {
1398 48
        $this->protectTagHelper($dom, 'code');
1399
1400 48
        foreach ($dom->find('script, style') as $element) {
1401 7
            if ($element->isRemoved()) {
1402
                continue;
1403
            }
1404
1405 7
            if ($element->tag === 'script' || $element->tag === 'style') {
1406 7
                $attributes = $element->getAllAttributes();
1407
                // skip external links
1408 7
                if (isset($attributes['src'])) {
1409 4
                    continue;
1410
                }
1411
            }
1412
1413 5
            $this->protectedChildNodes[$this->protected_tags_counter] = $element->innerhtml;
1414 5
            $element->getNode()->nodeValue = '<' . $this->protectedChildNodesHelper . ' data-' . $this->protectedChildNodesHelper . '="' . $this->protected_tags_counter . '"></' . $this->protectedChildNodesHelper . '>';
1415
1416 5
            ++$this->protected_tags_counter;
1417
        }
1418
1419 48
        foreach ($dom->find('//comment()') as $element) {
1420 4
            if ($element->isRemoved()) {
1421
                continue;
1422
            }
1423
1424 4
            $text = $element->text();
1425
1426
            // skip normal comments
1427 4
            if (!$this->isConditionalComment($text)) {
1428 4
                continue;
1429
            }
1430
1431 2
            $this->protectedChildNodes[$this->protected_tags_counter] = '<!--' . $text . '-->';
1432
1433
            /* @var $node \DOMComment */
1434 2
            $node = $element->getNode();
1435 2
            $child = new \DOMText('<' . $this->protectedChildNodesHelper . ' data-' . $this->protectedChildNodesHelper . '="' . $this->protected_tags_counter . '"></' . $this->protectedChildNodesHelper . '>');
1436
            /** @noinspection UnusedFunctionResultInspection */
1437 2
            $element->getNode()->parentNode->replaceChild($child, $node);
1438
1439 2
            ++$this->protected_tags_counter;
1440
        }
1441
1442 48
        return $dom;
1443
    }
1444
1445
    /**
1446
     * Remove comments in the dom.
1447
     *
1448
     * @param HtmlDomParser $dom
1449
     *
1450
     * @return HtmlDomParser
1451
     */
1452 46
    private function removeComments(HtmlDomParser $dom): HtmlDomParser
1453
    {
1454 46
        foreach ($dom->find('//comment()') as $commentWrapper) {
1455 3
            $comment = $commentWrapper->getNode();
1456 3
            $val = $comment->nodeValue;
1457 3
            if (\strpos($val, '[') === false) {
1458
                /** @noinspection UnusedFunctionResultInspection */
1459 3
                $comment->parentNode->removeChild($comment);
1460
            }
1461
        }
1462
1463 46
        $dom->getDocument()->normalizeDocument();
1464
1465 46
        return $dom;
1466
    }
1467
1468
    /**
1469
     * Trim tags in the dom.
1470
     *
1471
     * @param SimpleHtmlDomInterface $element
1472
     *
1473
     * @return void
1474
     */
1475 3
    private function removeWhitespaceAroundTags(SimpleHtmlDomInterface $element)
1476
    {
1477 3
        if (isset(self::$trimWhitespaceFromTags[$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...
1478 1
            $node = $element->getNode();
1479
1480
            /** @var \DOMNode[] $candidates */
1481 1
            $candidates = [];
1482 1
            if ($node->childNodes->length > 0) {
1483 1
                $candidates[] = $node->firstChild;
1484 1
                $candidates[] = $node->lastChild;
1485 1
                $candidates[] = $node->previousSibling;
1486 1
                $candidates[] = $node->nextSibling;
1487
            }
1488
1489
            /** @var mixed $candidate - false-positive error from phpstan */
1490 1
            foreach ($candidates as &$candidate) {
1491 1
                if ($candidate === null) {
1492
                    continue;
1493
                }
1494
1495 1
                if ($candidate->nodeType === \XML_TEXT_NODE) {
1496 1
                    $nodeValueTmp = \preg_replace(self::$regExSpace, ' ', $candidate->nodeValue);
1497 1
                    if ($nodeValueTmp !== null) {
1498 1
                        $candidate->nodeValue = $nodeValueTmp;
1499
                    }
1500
                }
1501
            }
1502
        }
1503 3
    }
1504
1505
    /**
1506
     * Callback function for preg_replace_callback use.
1507
     *
1508
     * @param array $matches PREG matches
1509
     *
1510
     * @return string
1511
     */
1512 9
    private function restoreProtectedHtml($matches): string
1513
    {
1514 9
        \preg_match('/.*"(?<id>\d*)"/', $matches['attributes'], $matchesInner);
1515
1516 9
        return $this->protectedChildNodes[$matchesInner['id']] ?? '';
1517
    }
1518
1519
    /**
1520
     * @param array $domainsToRemoveHttpPrefixFromAttributes
1521
     *
1522
     * @return $this
1523
     */
1524 2
    public function setDomainsToRemoveHttpPrefixFromAttributes($domainsToRemoveHttpPrefixFromAttributes): self
1525
    {
1526 2
        $this->domainsToRemoveHttpPrefixFromAttributes = $domainsToRemoveHttpPrefixFromAttributes;
1527
1528 2
        return $this;
1529
    }
1530
1531
    /**
1532
     * Sum-up extra whitespace from dom-nodes.
1533
     *
1534
     * @param HtmlDomParser $dom
1535
     *
1536
     * @return HtmlDomParser
1537
     */
1538 47
    private function sumUpWhitespace(HtmlDomParser $dom): HtmlDomParser
1539
    {
1540 47
        $text_nodes = $dom->find('//text()');
1541 47
        foreach ($text_nodes as $text_node_wrapper) {
1542
            /* @var $text_node \DOMNode */
1543 43
            $text_node = $text_node_wrapper->getNode();
1544 43
            $xp = $text_node->getNodePath();
1545 43
            if ($xp === null) {
1546
                continue;
1547
            }
1548
1549 43
            $doSkip = false;
1550 43
            foreach (self::$skipTagsForRemoveWhitespace as $pattern) {
1551 43
                if (\strpos($xp, "/${pattern}") !== false) {
1552 8
                    $doSkip = true;
1553
1554 8
                    break;
1555
                }
1556
            }
1557 43
            if ($doSkip) {
1558 8
                continue;
1559
            }
1560
1561 40
            $nodeValueTmp = \preg_replace(self::$regExSpace, ' ', $text_node->nodeValue);
1562 40
            if ($nodeValueTmp !== null) {
1563 40
                $text_node->nodeValue = $nodeValueTmp;
1564
            }
1565
        }
1566
1567 47
        $dom->getDocument()->normalizeDocument();
1568
1569 47
        return $dom;
1570
    }
1571
1572
    /**
1573
     * WARNING: maybe bad for performance ...
1574
     *
1575
     * @param bool $keepBrokenHtml
1576
     *
1577
     * @return HtmlMin
1578
     */
1579 2
    public function useKeepBrokenHtml(bool $keepBrokenHtml): self
1580
    {
1581 2
        $this->keepBrokenHtml = $keepBrokenHtml;
1582
1583 2
        return $this;
1584
    }
1585
}
1586