Passed
Push — master ( bc0996...c35245 )
by Bruno
05:47
created

HTMLElement   F

Complexity

Total Complexity 84

Size/Duplication

Total Lines 565
Duplicated Lines 0 %

Test Coverage

Coverage 78.8%

Importance

Changes 2
Bugs 2 Features 0
Metric Value
eloc 166
c 2
b 2
f 0
dl 0
loc 565
ccs 145
cts 184
cp 0.788
rs 2
wmc 84

30 Methods

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

How to fix   Complexity   

Complex Class

Complex classes like HTMLElement 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 HTMLElement, and based on these observations, apply Extract Interface, too.

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