HTMLNode   F
last analyzed

Complexity

Total Complexity 90

Size/Duplication

Total Lines 605
Duplicated Lines 0 %

Test Coverage

Coverage 78.46%

Importance

Changes 1
Bugs 1 Features 0
Metric Value
wmc 90
eloc 175
c 1
b 1
f 0
dl 0
loc 605
ccs 153
cts 195
cp 0.7846
rs 2

33 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 2
A replace() 0 5 1
A appendContent() 0 4 1
A isEmpty() 0 3 1
A getMetadata() 0 3 1
A getCountChildren() 0 3 1
A setAttribute() 0 4 1
A clearContent() 0 4 1
A getContent() 0 3 1
A addContent() 0 4 1
B setContent() 0 25 7
A setAttributes() 0 20 5
A map() 0 11 4
A setRenderIfEmpty() 0 4 1
A getElements() 0 3 1
A addAttribute() 0 4 1
A __toString() 0 3 1
A __clone() 0 8 3
A getTag() 0 3 1
A factory() 0 3 1
A getInternal() 0 15 4
A addAttributes() 0 4 1
A setTag() 0 4 1
A removeAttribute() 0 6 2
A hasAttribute() 0 3 1
A filter() 0 14 5
A getAttribute() 0 3 1
A prependContent() 0 4 1
A clearAttributes() 0 4 1
B match() 0 23 7
B get() 0 27 6
A walk() 0 9 3
F getRenderHTML() 0 64 21

How to fix   Complexity   

Complex Class

Complex classes like HTMLNode 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.

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 HTMLNode, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
namespace Formularium;
4
5
use PHP_CodeSniffer\Generators\HTML;
6
7
use function Safe\substr;
8
9
/**
10
 * Class that encapsule DOM node elements. Similar to PHP DOMElement but more flexible.
11
 * This is not used for parsing, but to build HTML.
12
 */
13
class HTMLNode
14
{
15
    const STANDALONE_TAGS = ['img', 'hr', 'br', 'input', 'meta', 'col', 'command', 'link', 'param', 'source', 'embed'];
16
17
    /**
18
     * The HTML attributes and respectives values
19
     * This is an associative array wich:
20
     *     key is the attribute and
21
     *     value is the array with attributes values
22
     * @var array
23
     */
24
    protected $attributes = [];
25
26
    /**
27
     * Tag name
28
     * @var string
29
     */
30
    protected $tag;
31
32
    /**
33
     * The content of Element
34
     * @var array
35
     */
36
    protected $content = [];
37
38
    /**
39
     * If this tag has no children, still render it?
40
     * @var boolean
41
     */
42
    protected $renderIfEmpty = true;
43
44
    /**
45
     * Extra data stored with this node that might be used by the frameworks.
46
     *
47
     * @var Metadata
48
     */
49
    protected $metadata = null;
50
51
    /**
52
     * Create a HTML Element
53
     * @param string $tag The tag name of Element
54
     * @param array $attributes The attribute with values
55
     * @param mixed $content The content of element, can be:
56
     *                - string (with text content)
57
     *                - HTMLNode
58
     *                - array with others elements or text
59
     * @param boolean $raw If true, do not escape content.
60
     */
61 13
    public function __construct(string $tag, array $attributes = [], $content = '', $raw = false)
62
    {
63 13
        $this->tag = $tag;
64 13
        $this->metadata = new Metadata('HTMLNode', '', []);
65
66 13
        $this->setAttributes($attributes);
67
68 13
        if (!empty($content)) {
69 7
            $this->setContent($content, true, $raw);
70
        }
71 13
    }
72
73
    /**
74
     * Create a HTML Element
75
     * @param string $tag The tag name of Element
76
     * @param array $attributes The attribute with values
77
     * @param mixed $content The content of element, can be:
78
     *                - string (with text content)
79
     *                - HTMLNode
80
     *                - array with others elements or text
81
     * @param boolean $raw If true, do not escape content.
82
     * @return HTMLNode
83
     */
84 3
    public static function factory(string $tag, array $attributes = [], $content = '', $raw = false): HTMLNode
85
    {
86 3
        return new self($tag, $attributes, $content, $raw);
87
    }
88
89
    /**
90
     * Sets whether this tag will render if it is empty (that is,
91
     * no contents or children)
92
     *
93
     * @param boolean $val
94
     * @return HTMLNode Itself
95
     */
96
    public function setRenderIfEmpty(bool $val): HTMLNode
97
    {
98
        $this->renderIfEmpty = $val;
99
        return $this;
100
    }
101
102
    /**
103
     * Gets metadata.
104
     * @return Metadata
105
     */
106
    public function getMetadata(): Metadata
107
    {
108
        return $this->metadata;
109
    }
110
111
    /**
112
     * Return a tag
113
     * @return string tag name
114
     */
115 4
    public function getTag()
116
    {
117 4
        return $this->tag;
118
    }
119
120
    /**
121
     * Modifies our tag
122
     *
123
     * @param string $tag
124
     * @return HTMLNode Itself
125
     */
126 1
    public function setTag($tag)
127
    {
128 1
        $this->tag = $tag;
129 1
        return $this;
130
    }
131
132
    /**
133
     * Return a attribute
134
     * @param string $name The name of attribute
135
     * @return array Return the list of values, if attribute don't exist return empty array
136
     */
137 2
    public function getAttribute($name): array
138
    {
139 2
        return $this->attributes[$name] ?? [];
140
    }
141
142
    /**
143
     * Does attribute exist?
144
     * @param string $name The name of attribute
145
     * @return bool
146
     */
147
    public function hasAttribute($name): bool
148
    {
149
        return array_key_exists($name, $this->attributes);
150
    }
151
152
    /**
153
     * Return the content of element
154
     * @return mixed array of HTMLNode and string (text)
155
     */
156 4
    public function getContent()
157
    {
158 4
        return $this->content;
159
    }
160
161
    public function isEmpty(): bool
162
    {
163
        return count($this->content) > 0;
164
    }
165
166
    /**
167
     *
168
     * @return int
169
     */
170
    public function getCountChildren(): int
171
    {
172
        return count($this->content);
173
    }
174
175
    /**
176
     * Set a attribute value, if attribute exist overwrite it
177
     * @param string $name
178
     * @param mixed $value Can be a string or array of string
179
     * @return HTMLNode Itself
180
     */
181 1
    public function setAttribute(string $name, $value): HTMLNode
182
    {
183 1
        $this->setAttributes([$name => $value], true);
184 1
        return $this;
185
    }
186
187
    /**
188
     * Add an attribute value, if attribute exist append value
189
     * @param string $name
190
     * @param mixed $value Can be a string or array of string
191
     * @return HTMLNode Itself
192
     */
193 1
    public function addAttribute(string $name, $value = null): HTMLNode
194
    {
195 1
        $this->setAttributes([$name => $value], false);
196 1
        return $this;
197
    }
198
199
    /**
200
     * Set a set of attributes
201
     * @param array $attributes Associative array wich
202
     * 				key is attributes names
203
     *              value is string or array values
204
     * @param bool $overwrite if true and attribute name exist overwrite them
205
     * @return HTMLNode Itself
206
     *
207
     */
208 13
    public function setAttributes(array $attributes, $overwrite = true): HTMLNode
209
    {
210 13
        foreach ($attributes as $atrib => $value) {
211 12
            if (is_array($value)) {
212 2
                $values = $value;
213
            } else {
214 12
                $values = [$value];
215
            }
216
217 12
            if ($overwrite) {
218 12
                $this->attributes[$atrib] = $values;
219
            } else {
220 1
                if (empty($this->attributes[$atrib])) {
221 1
                    $this->attributes[$atrib] = [];
222
                }
223
224 1
                $this->attributes[$atrib] = array_merge($this->attributes[$atrib], $values);
225
            }
226
        }
227 13
        return $this;
228
    }
229
230
    public function removeAttribute(string $attribute): HTMLNode
231
    {
232
        if (array_key_exists($attribute, $this->attributes)) {
233
            unset($this->attributes[$attribute]);
234
        }
235
        return $this;
236
    }
237
238
    /**
239
     * Aliases to HTMLNode::setAttributes($content, false);
240
     * @see setAttributes
241
     * @return HTMLNode Itself
242
     */
243 1
    public function addAttributes(array $attributes): HTMLNode
244
    {
245 1
        $this->setAttributes($attributes, false);
246 1
        return $this;
247
    }
248
249
    /**
250
     * Set content (dom objects or texts) to element
251
     * @param string|HTMLNode|string[]|HTMLNode[] $content The content of element, can be:
252
     *                - string (with text content)
253
     *                - HTMLNode
254
     *                - array with others elements or text
255
     * @param bool $overwrite if true overwrite content otherwise append the content
256
     * @param bool $raw If true, this is raw content (html) and should not be escaped.
257
     * @param bool $prepend If true prepend instead of appending
258
     * @return HTMLNode Itself
259
     */
260 11
    public function setContent($content, $overwrite = true, $raw = false, $prepend = false): HTMLNode
261
    {
262
        // TODO Don't work with reference objects, change it
263 11
        if (!is_array($content)) {
264 8
            $content = [$content];
265
        }
266
267 11
        if ($raw === false) {
268 9
            foreach ($content as &$item) {
269 9
                if (is_string($item)) {
270 8
                    $item = htmlspecialchars($item);
271
                }
272
            }
273
        }
274
275 11
        if ($overwrite) {
276 8
            $this->content = $content;
277
        } else {
278 5
            if ($prepend) {
279
                $this->content = array_merge($content, $this->content);
280
            } else {
281 5
                $this->content = array_merge($this->content, $content);
282
            }
283
        }
284 11
        return $this;
285
    }
286
287
    /**
288
     * Aliases to HTMLNode::setContent($content, false);
289
     * @see setContent
290
     * @param string|HTMLNode|string[]|HTMLNode[] $content
291
     * @return HTMLNode Itself
292
     */
293 3
    public function addContent($content, bool $raw = false): HTMLNode
294
    {
295 3
        $this->setContent($content, false, $raw);
296 3
        return $this;
297
    }
298
299
    /**
300
     * Appends content nodes to the bottom of this element.
301
     *
302
     * @see setContent
303
     * @param string|HTMLNode|string[]|HTMLNode[] $content
304
     * @param boolean $raw
305
     * @return HTMLNode
306
     */
307 1
    public function appendContent($content, bool $raw = false): HTMLNode
308
    {
309 1
        $this->setContent($content, false, $raw);
310 1
        return $this;
311
    }
312
313
    /**
314
     * Prepends content nodes to the beginning of this element.
315
     *
316
     * @see setContent
317
     * @param string|HTMLNode|string[]|HTMLNode[] $content
318
     * @param boolean $raw
319
     * @return HTMLNode
320
     */
321
    public function prependContent($content, bool $raw = false): HTMLNode
322
    {
323
        $this->setContent($content, false, $raw, true);
324
        return $this;
325
    }
326
327
    /**
328
     * Find and return elements using selector
329
     * @param string $selector A selector of elements based in jQuery
330
     *						'eee' - Select elements 'eee' (with tag)
331
     * 						'#ii' - Select a element with id attribute 'ii'
332
     * 						'.cc' - Select elements with class attribute 'cc'
333
     * 						'[a=v]' - Select elements with 'a' attribute with 'v' value
334
     * 						'e#i' - Select elements 'e' with id attribute 'i'
335
     * 						'e.c' - Select elements 'e' with class attribute 'c'
336
     * @return HTMLNode[]
337
     */
338 2
    public function get(string $selector)
339
    {
340 2
        $tag = null;
341 2
        $attr = null;
342 2
        $val = null;
343
344
        // Define what could be found
345 2
        $selector = trim($selector);
346 2
        if ($selector[0] == "#") {
347 1
            $attr = "id";
348 1
            $val = mb_substr($selector, 1);
349 2
        } elseif ($selector[0] == ".") {
350 2
            $attr = "class";
351 2
            $val = mb_substr($selector, 1);
352 1
        } elseif (mb_strpos($selector, "=") !== false) {
353 1
            list($attr, $val) = explode("=", substr($selector, 1, -1));
354 1
        } elseif (mb_strpos($selector, "#") !== false) {
355 1
            $attr = "id";
356 1
            list($tag, $val) = explode("#", $selector);
357 1
        } elseif (mb_strpos($selector, ".") !== false) {
358 1
            $attr = "class";
359 1
            list($tag, $val) = explode(".", $selector);
360
        } else {
361 1
            $tag = $selector;
362
        }
363
364 2
        return $this->getInternal($this, $tag, $attr, $val);
365
    }
366
367
    /**
368
     * Find and return elements based in $tag, $attr, $val
369
     * The $tag or $attr must be a value
370
     *
371
     * @see HTMLNode::get
372
     * @param string $tag tag of search
373
     * @param string $attr attribute of search
374
     * @param string $val value of attribute search
375
     * @return HTMLNode[]
376
     */
377 1
    public function getElements(string $tag, string $attr, string $val)
378
    {
379 1
        return $this->getInternal($this, $tag, $attr, $val);
380
    }
381
382
    /**
383
     * Recursive function to found elements
384
     * @param HTMLNode $element Element that will be available
385
     * @param string $tag Tag or null value to compare
386
     * @param string $attr Attribute name or null value to compare
387
     * @param string $val Value of attribute or null value to compare
388
     * @return HTMLNode[]
389
     */
390 2
    protected function getInternal(HTMLNode $element, string $tag = null, string $attr = null, string $val = null)
391
    {
392 2
        if ($this->match($element, $tag, $attr, $val)) {
393 2
            $return = [$element];
394
        } else {
395 2
            $return = [];
396
        }
397
398 2
        foreach ($element->getContent() as $content) {
399 2
            if ($content instanceof HTMLNode) {
400 2
                $return = array_merge($return, $this->getInternal($content, $tag, $attr, $val));
401
            }
402
        }
403
404 2
        return $return;
405
    }
406
407
    /**
408
     * Return a boolean based on match of the element with $tag, $attr or $val
409
     * @param HTMLNode $element Element that will be available
410
     * @param string $tag Tag or null value to compare
411
     * @param string $attr Attribute name or null value to compare
412
     * @param string $val Value of attribute or null value to compare
413
     * @return boolean - true when satisfy and false otherwise
414
     */
415 2
    protected function match(HTMLNode $element, $tag, $attr, $val): bool
416
    {
417 2
        if (!empty($tag)) {
418 1
            if ($element->getTag() != $tag) {
419 1
                return false;
420
            }
421
        } elseif (empty($attr)) {
422
            // Only when $tag and $attr is empty
423 1
            return false;
424
        }
425
426 2
        if (!empty($attr)) {
427 2
            $values = $element->getAttribute($attr);
428 2
            if (count($values) == 0) {
429 2
                return false;
430
            }
431
432 2
            if (!empty($val)) {
433 2
                return in_array($val, $values);
434
            }
435
        }
436
437 1
        return true;
438
    }
439
440 2
    public function __toString()
441
    {
442 2
        return $this->getRenderHTML();
443
    }
444
    
445
    /**
446
     * Return the html element code including all children
447
     *
448
     * @param string $indentString String used to indent HTML code. Use '' for a compact version.
449
     * @param integer $level The current indentation level.
450
     * @return string (html code)
451
     */
452 9
    public function getRenderHTML($indentString = '  ', $level = 0): string
453
    {
454
455
        // skip empty non renderable
456 9
        if ($this->renderIfEmpty === false) {
457
            if (!count($this->content)) {
458
                return '';
459
            }
460
        }
461
462
        // start
463 9
        $data = [];
464
465
        // if this is not empty, the tag
466 9
        if (!empty($this->tag)) {
467 9
            $open = [];
468 9
            $open[] = ($level > 0 ? $indentString : '') . // initial indentation
469 9
                '<' . htmlspecialchars($this->tag);
470
471
            // render tag attributes
472 9
            foreach ($this->attributes as $attrib => $value) {
473 8
                $open[] = $attrib . ($value === [null] ? '' : ('="' . htmlspecialchars(implode(' ', $value)) . '"'));
474
            }
475 9
            $data[] = join(' ', $open) . (in_array($this->tag, self::STANDALONE_TAGS) ? '/>' : '>');
476
        }
477
478
        // recurse
479 9
        $contentdata = [];
480 9
        $emptyfieldset = ($this->tag == 'fieldset'); // avoid rendering fieldset with only a "legend"
481 9
        foreach ($this->content as $content) {
482 6
            if ($content instanceof HTMLNode) {
483 4
                $c = $content->getRenderHTML($indentString, $level + 1);
484 4
                if ($this->tag == 'fieldset' and $content->getTag() != 'legend' and $c) {
485
                    $emptyfieldset = false;
486
                }
487 4
                $contentdata[] = $c;
488
            } else {
489 5
                $emptyfieldset = false;
490 5
                $contentdata[] = $indentString . $content;
491
            }
492
        }
493
494
        // handle special empty cases
495 9
        if ($emptyfieldset) {
496
            return '';
497 9
        } elseif ($contentdata == [] && $this->renderIfEmpty === false) {
498
            return '';
499
        }
500
501
        // join content
502 9
        $data = array_merge($data, $contentdata);
503
504
        // handle closing
505 9
        if (!empty($this->tag)
506 9
            && !in_array($this->tag, self::STANDALONE_TAGS)
507
        ) {
508 9
            $data[] = '</' . htmlspecialchars($this->tag) . '>';
509
        }
510
511 9
        if ($indentString && $level === 0) {
512 4
            $data[] = "\n";
513
        }
514
515 9
        return implode(($indentString ? "\n" : '') . str_repeat($indentString, $level), $data);
516
    }
517
518
    /**
519
     * Clone HTMLNode object and its child
520
     * @return Object HTMLNode
521
     */
522
    public function __clone()
523
    {
524
        $obj = new HTMLNode($this->tag, $this->attributes);
525
        foreach ($this->content as $content) {
526
            if ($content instanceof HTMLNode) {
527
                $obj->addContent(clone $content);
528
            } else {
529
                $obj->addContent($content);
530
            }
531
        }
532
    }
533
534
    public function replace(HTMLNode $e): void
535
    {
536
        $this->tag = $e->tag;
537
        $this->attributes = $e->attributes;
538
        $this->content = $e->content;
539
    }
540
541
    /**
542
     * Clear All Attributes
543
     */
544
    public function clearAttributes(): HTMLNode
545
    {
546
        $this->attributes = [];
547
        return $this;
548
    }
549
550
551
    /**
552
     * Clear All Content
553
     */
554 1
    public function clearContent(): HTMLNode
555
    {
556 1
        $this->content = [];
557 1
        return $this;
558
    }
559
560
    /**
561
     * Similar to array_walk(). Applied to this HTMLNode and all its children.
562
     * Does not call callback for text content.
563
     *
564
     * @param callable $f
565
     * @return HTMLNode self
566
     */
567 1
    public function walk(callable $f): HTMLNode
568
    {
569 1
        $f($this);
570 1
        foreach ($this->content as $content) {
571 1
            if ($content instanceof HTMLNode) {
572 1
                $content->walk($f);
573
            }
574
        }
575 1
        return $this;
576
    }
577
578
    /**
579
     * Similar to array_map(). Calls callback for text content too.
580
     *
581
     * @param callable $f
582
     * @return array
583
     */
584 1
    public function map(callable $f, bool $recurse = true): array
585
    {
586 1
        $data = [$f($this)];
587 1
        foreach ($this->content as $content) {
588 1
            if ($recurse && $content instanceof HTMLNode) {
589 1
                $data = array_merge($data, $content->map($f, $recurse));
590
            } else {
591 1
                $data[] = $f($content);
592
            }
593
        }
594 1
        return $data;
595
    }
596
597
    /**
598
     * Similar to array_filter(): removes children from this element.
599
     * Does not call callback for text content.
600
     *
601
     * @param callable $f If returns false, element being checked is removed.
602
     * @return HTMLNode[] The filtered elements.
603
     */
604 1
    public function filter(callable $f, bool $recurse = true): array
605
    {
606 1
        $deleted = [];
607 1
        foreach ($this->content as $key => $content) {
608 1
            if ($content instanceof HTMLNode) {
609 1
                if (!$f($content)) {
610 1
                    $deleted[] = $this->content[$key];
611 1
                    unset($this->content[$key]);
612
                } elseif ($recurse) {
613
                    $content->filter($f, $recurse);
614
                }
615
            }
616
        }
617 1
        return $deleted;
618
    }
619
}
620