Passed
Push — master ( 0a0bcf...92e6c5 )
by Bruno
06:17
created

HTMLElement::getRenderHTML()   F

Complexity

Conditions 20
Paths 145

Size

Total Lines 64
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 22.4034

Importance

Changes 0
Metric Value
cc 20
eloc 33
c 0
b 0
f 0
nc 145
nop 2
dl 0
loc 64
ccs 27
cts 33
cp 0.8182
crap 22.4034
rs 3.7916

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