BaseElement::addChildren()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 14

Duplication

Lines 14
Ratio 100 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 14
loc 14
rs 9.7998
c 0
b 0
f 0
1
<?php
2
3
namespace Spatie\Html;
4
5
use BadMethodCallException;
6
use Illuminate\Contracts\Support\Htmlable;
7
use Illuminate\Support\Collection;
8
use Illuminate\Support\HtmlString;
9
use Illuminate\Support\Str;
10
use Illuminate\Support\Traits\Macroable;
11
use Spatie\Html\Exceptions\InvalidChild;
12
use Spatie\Html\Exceptions\InvalidHtml;
13
use Spatie\Html\Exceptions\MissingTag;
14
15
abstract class BaseElement implements Htmlable, HtmlElement
16
{
17
    use Macroable {
18
        __call as __macro_call;
19
    }
20
21
    /** @var string */
22
    protected $tag;
23
24
    /** @var \Spatie\Html\Attributes */
25
    protected $attributes;
26
27
    /** @var \Illuminate\Support\Collection */
28
    protected $children;
29
30
    public function __construct()
31
    {
32
        if (empty($this->tag)) {
33
            throw MissingTag::onClass(static::class);
34
        }
35
36
        $this->attributes = new Attributes();
37
        $this->children = new Collection();
38
    }
39
40
    public static function create()
41
    {
42
        return new static();
43
    }
44
45
    /**
46
     * @param string $attribute
47
     * @param string|null $value
48
     *
49
     * @return static
50
     */
51
    public function attribute($attribute, $value = null)
52
    {
53
        $element = clone $this;
54
55
        $element->attributes->setAttribute($attribute, (string) $value);
56
57
        return $element;
58
    }
59
60
    /**
61
     * @param iterable $attributes
62
     *
63
     * @return static
64
     */
65
    public function attributes($attributes)
66
    {
67
        $element = clone $this;
68
69
        $element->attributes->setAttributes($attributes);
70
71
        return $element;
72
    }
73
74
    /**
75
     * @param string $attribute
76
     *
77
     * @return static
78
     */
79
    public function forgetAttribute($attribute)
80
    {
81
        $element = clone $this;
82
83
        $element->attributes->forgetAttribute($attribute);
84
85
        return $element;
86
    }
87
88
    /**
89
     * @param string $attribute
90
     * @param mixed $fallback
91
     *
92
     * @return mixed
93
     */
94
    public function getAttribute($attribute, $fallback = null)
95
    {
96
        return $this->attributes->getAttribute($attribute, $fallback);
97
    }
98
99
    /**
100
     * @param string $attribute
101
     *
102
     * @return bool
103
     */
104
    public function hasAttribute($attribute)
105
    {
106
        return $this->attributes->hasAttribute($attribute);
107
    }
108
109
    /**
110
     * @param iterable|string $class
111
     *
112
     * @return static
113
     */
114
    public function class($class)
115
    {
116
        return $this->addClass($class);
117
    }
118
119
    /**
120
     * Alias for `class`.
121
     *
122
     * @param iterable|string $class
123
     *
124
     * @return static
125
     */
126
    public function addClass($class)
127
    {
128
        $element = clone $this;
129
130
        $element->attributes->addClass($class);
131
132
        return $element;
133
    }
134
135
    /**
136
     * @param string $id
137
     *
138
     * @return static
139
     */
140
    public function id($id)
141
    {
142
        return $this->attribute('id', $id);
143
    }
144
145
    /**
146
     * @param array|string|null $style
147
     *
148
     * @return static
149
     */
150
    public function style($style)
151
    {
152
        if (is_array($style)) {
153
            $style = implode('; ', array_map(function ($value, $attribute) {
154
                return "{$attribute}: {$value}";
155
            }, $style, array_keys($style)));
156
        }
157
158
        return $this->attribute('style', $style);
159
    }
160
161
    /**
162
     * @param string $name
163
     * @param string $value
164
     *
165
     * @return static
166
     */
167
    public function data($name, $value = null)
168
    {
169
        return $this->attribute("data-{$name}", $value);
170
    }
171
172
    /**
173
     * @param \Spatie\Html\HtmlElement|string|iterable|null $children
174
     * @param callable|null $mapper
175
     *
176
     * @return static
177
     */
178 View Code Duplication
    public function addChildren($children, $mapper = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
179
    {
180
        if (is_null($children)) {
181
            return $this;
182
        }
183
184
        $children = $this->parseChildren($children, $mapper);
185
186
        $element = clone $this;
187
188
        $element->children = $element->children->merge($children);
189
190
        return $element;
191
    }
192
193
    /**
194
     * Alias for `addChildren`.
195
     *
196
     * @param \Spatie\Html\HtmlElement|string|iterable|null $children
0 ignored issues
show
Bug introduced by
There is no parameter named $children. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
197
     * @param callable|null $mapper
198
     *
199
     * @return static
200
     */
201
    public function addChild($child, $mapper = null)
202
    {
203
        return $this->addChildren($child, $mapper);
204
    }
205
206
    /**
207
     * Alias for `addChildren`.
208
     *
209
     * @param \Spatie\Html\HtmlElement|string|iterable|null $children
0 ignored issues
show
Bug introduced by
There is no parameter named $children. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
210
     * @param callable|null $mapper
211
     *
212
     * @return static
213
     */
214
    public function child($child, $mapper = null)
215
    {
216
        return $this->addChildren($child, $mapper);
217
    }
218
219
    /**
220
     * Alias for `addChildren`.
221
     *
222
     * @param \Spatie\Html\HtmlElement|string|iterable|null $children
223
     * @param callable|null $mapper
224
     *
225
     * @return static
226
     */
227
    public function children($children, $mapper = null)
228
    {
229
        return $this->addChildren($children, $mapper);
230
    }
231
232
    /**
233
     * Replace all children with an array of elements.
234
     *
235
     * @param \Spatie\Html\HtmlElement[] $children
236
     * @param callable|null $mapper
237
     *
238
     * @return static
239
     */
240
    public function setChildren($children, $mapper = null)
241
    {
242
        $element = clone $this;
243
244
        $element->children = new Collection();
245
246
        return $element->addChildren($children, $mapper);
0 ignored issues
show
Documentation introduced by
$children is of type array<integer,object<Spatie\Html\HtmlElement>>, but the function expects a object<Spatie\Html\HtmlE...tie\Html\iterable>|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
247
    }
248
249
    /**
250
     * @param \Spatie\Html\HtmlElement|string|iterable|null $children
251
     * @param callable|null $mapper
252
     *
253
     * @return static
254
     */
255 View Code Duplication
    public function prependChildren($children, $mapper = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
256
    {
257
        $children = $this->parseChildren($children, $mapper);
258
259
        $element = clone $this;
260
261
        $element->children = $children->merge($element->children);
262
263
        return $element;
264
    }
265
266
    /**
267
     * Alias for `prependChildren`.
268
     *
269
     * @param \Spatie\Html\HtmlElement|string|iterable|null $children
270
     * @param callable|null $mapper
271
     *
272
     * @return static
273
     */
274
    public function prependChild($children, $mapper = null)
275
    {
276
        return $this->prependChildren($children, $mapper);
277
    }
278
279
    /**
280
     * @param string|null $text
281
     *
282
     * @return static
283
     */
284
    public function text($text)
285
    {
286
        return $this->html(htmlentities($text, ENT_QUOTES, 'UTF-8', false));
287
    }
288
289
    /**
290
     * @param string|null $html
291
     *
292
     * @return static
293
     */
294
    public function html($html)
295
    {
296
        if ($this->isVoidElement()) {
297
            throw new InvalidHtml("Can't set inner contents on `{$this->tag}` because it's a void element");
298
        }
299
300
        return $this->setChildren($html);
0 ignored issues
show
Documentation introduced by
$html is of type string|null, but the function expects a array<integer,object<Spatie\Html\HtmlElement>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
301
    }
302
303
    /**
304
     * Conditionally transform the element. Note that since elements are
305
     * immutable, you'll need to return a new instance from the callback.
306
     *
307
     * @param bool $condition
308
     * @param \Closure $callback
309
     *
310
     * @return mixed
311
     */
312
    public function if(bool $condition, \Closure $callback)
313
    {
314
        return $condition ? $callback($this) : $this;
315
    }
316
317
    /**
318
     * Conditionally transform the element. Note that since elements are
319
     * immutable, you'll need to return a new instance from the callback.
320
     *
321
     * @param bool $condition
322
     * @param \Closure $callback
323
     *
324
     * @return mixed
325
     */
326
    public function unless(bool $condition, \Closure $callback)
327
    {
328
        return $this->if(! $condition, $callback);
329
    }
330
331
    /**
332
     * Conditionally transform the element. Note that since elements are
333
     * immutable, you'll need to return a new instance from the callback.
334
     *
335
     * @param mixed $value
336
     * @param \Closure $callback
337
     *
338
     * @return mixed
339
     */
340
    public function ifNotNull($value, \Closure $callback)
341
    {
342
        return ! is_null($value) ? $callback($this) : $this;
343
    }
344
345
    /**
346
     * @return \Illuminate\Contracts\Support\Htmlable
347
     */
348
    public function open()
349
    {
350
        $tag = $this->attributes->isEmpty()
351
            ? '<'.$this->tag.'>'
352
            : "<{$this->tag} {$this->attributes->render()}>";
353
354
        $children = $this->children->map(function ($child): string {
355
            if ($child instanceof HtmlElement) {
356
                return $child->render();
357
            }
358
359
            if (is_null($child)) {
360
                return '';
361
            }
362
363
            if (is_string($child)) {
364
                return $child;
365
            }
366
367
            throw InvalidChild::childMustBeAnHtmlElementOrAString();
368
        })->implode('');
369
370
        return new HtmlString($tag.$children);
371
    }
372
373
    /**
374
     * @return \Illuminate\Contracts\Support\Htmlable
375
     */
376
    public function close()
377
    {
378
        return new HtmlString(
379
            $this->isVoidElement()
380
                ? ''
381
                : "</{$this->tag}>"
382
        );
383
    }
384
385
    /**
386
     * @return \Illuminate\Contracts\Support\Htmlable
387
     */
388
    public function render()
389
    {
390
        return new HtmlString(
391
            $this->open().$this->close()
392
        );
393
    }
394
395
    public function isVoidElement(): bool
396
    {
397
        return in_array($this->tag, [
398
            'area', 'base', 'br', 'col', 'embed', 'hr',
399
            'img', 'input', 'keygen', 'link', 'menuitem',
400
            'meta', 'param', 'source', 'track', 'wbr',
401
        ]);
402
    }
403
404
    /**
405
     * Dynamically handle calls to the class.
406
     * Check for methods finishing by If or fallback to Macroable.
407
     *
408
     * @param  string  $name
409
     * @param  array   $arguments
410
     * @return mixed
411
     *
412
     * @throws BadMethodCallException
413
     */
414
    public function __call($name, $arguments)
415
    {
416
        if (Str::endsWith($name, $conditions = ['If', 'Unless', 'IfNotNull'])) {
417
            foreach ($conditions as $condition) {
418
                if (! method_exists($this, $method = str_replace($condition, '', $name))) {
419
                    continue;
420
                }
421
422
                return $this->callConditionalMethod($condition, $method, $arguments);
423
            }
424
        }
425
426
        return $this->__macro_call($name, $arguments);
427
    }
428
429
    protected function callConditionalMethod($type, $method, array $arguments)
430
    {
431
        $value = array_shift($arguments);
432
        $callback = function () use ($method, $arguments) {
433
            return $this->{$method}(...$arguments);
434
        };
435
436
        switch ($type) {
437
            case 'If':
438
                return $this->if((bool) $value, $callback);
439
            case 'Unless':
440
                return $this->unless((bool) $value, $callback);
441
            case 'IfNotNull':
442
                return $this->ifNotNull($value, $callback);
443
            default:
444
                return $this;
445
        }
446
    }
447
448
    public function __clone()
449
    {
450
        $this->attributes = clone $this->attributes;
451
        $this->children = clone $this->children;
452
    }
453
454
    public function __toString(): string
455
    {
456
        return $this->render();
457
    }
458
459
    public function toHtml(): string
460
    {
461
        return $this->render();
462
    }
463
464
    protected function parseChildren($children, $mapper = null): Collection
465
    {
466
        if ($children instanceof HtmlElement) {
467
            $children = [$children];
468
        }
469
470
        $children = Collection::make($children);
471
472
        if ($mapper) {
473
            $children = $children->map($mapper);
474
        }
475
476
        $this->guardAgainstInvalidChildren($children);
477
478
        return $children;
479
    }
480
481
    protected function guardAgainstInvalidChildren(Collection $children)
482
    {
483
        foreach ($children as $child) {
484
            if ((! $child instanceof HtmlElement) && (! is_string($child)) && (! is_null($child))) {
485
                throw InvalidChild::childMustBeAnHtmlElementOrAString();
486
            }
487
        }
488
    }
489
}
490