Completed
Push — master ( 821a00...b420eb )
by Sebastian
01:52 queued 47s
created

BaseElement::toHtml()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Spatie\Html;
4
5
use BadMethodCallException;
6
use Illuminate\Support\Collection;
7
use Illuminate\Support\HtmlString;
8
use Spatie\Html\Exceptions\MissingTag;
9
use Spatie\Html\Exceptions\InvalidHtml;
10
use Illuminate\Support\Traits\Macroable;
11
use Spatie\Html\Exceptions\InvalidChild;
12
use Illuminate\Contracts\Support\Htmlable;
13
14
abstract class BaseElement implements Htmlable, HtmlElement
15
{
16
    use Macroable {
17
        __call as __macro_call;
18
    }
19
20
    /** @var string */
21
    protected $tag;
22
23
    /** @var \Spatie\Html\Attributes */
24
    protected $attributes;
25
26
    /** @var \Illuminate\Support\Collection */
27
    protected $children;
28
29
    public function __construct()
30
    {
31
        if (empty($this->tag)) {
32
            throw MissingTag::onClass(static::class);
33
        }
34
35
        $this->attributes = new Attributes();
36
        $this->children = new Collection();
37
    }
38
39
    public static function create()
40
    {
41
        return new static();
42
    }
43
44
    /**
45
     * @param string $attribute
46
     * @param string|null $value
47
     *
48
     * @return static
49
     */
50
    public function attribute($attribute, $value = null)
51
    {
52
        $element = clone $this;
53
54
        $element->attributes->setAttribute($attribute, (string) $value);
55
56
        return $element;
57
    }
58
59
    /**
60
     * @param iterable $attributes
61
     *
62
     * @return static
63
     */
64
    public function attributes($attributes)
65
    {
66
        $element = clone $this;
67
68
        $element->attributes->setAttributes($attributes);
69
70
        return $element;
71
    }
72
73
    /**
74
     * @param string $attribute
75
     *
76
     * @return static
77
     */
78
    public function forgetAttribute($attribute)
79
    {
80
        $element = clone $this;
81
82
        $element->attributes->forgetAttribute($attribute);
83
84
        return $element;
85
    }
86
87
    /**
88
     * @param string $attribute
89
     * @param mixed $fallback
90
     *
91
     * @return mixed
92
     */
93
    public function getAttribute($attribute, $fallback = null)
94
    {
95
        return $this->attributes->getAttribute($attribute, $fallback);
96
    }
97
98
    /**
99
     * @param string $attribute
100
     *
101
     * @return bool
102
     */
103
    public function hasAttribute($attribute)
104
    {
105
        return $this->attributes->hasAttribute($attribute);
106
    }
107
108
    /**
109
     * @param iterable|string $class
110
     *
111
     * @return static
112
     */
113
    public function class($class)
114
    {
115
        return $this->addClass($class);
116
    }
117
118
    /**
119
     * Alias for `class`.
120
     *
121
     * @param iterable|string $class
122
     *
123
     * @return static
124
     */
125
    public function addClass($class)
126
    {
127
        $element = clone $this;
128
129
        $element->attributes->addClass($class);
130
131
        return $element;
132
    }
133
134
    /**
135
     * @param string $id
136
     *
137
     * @return static
138
     */
139
    public function id($id)
140
    {
141
        return $this->attribute('id', $id);
142
    }
143
144
    /**
145
     * @param array|string|null $style
146
     *
147
     * @return static
148
     */
149
    public function style($style)
150
    {
151
        if (is_array($style)) {
152
            $style = implode('; ', array_map(function ($value, $attribute) {
153
                return "{$attribute}: {$value}";
154
            }, $style, array_keys($style)));
155
        }
156
157
        return $this->attribute('style', $style);
158
    }
159
160
    /**
161
     * @param string $name
162
     * @param string $value
163
     *
164
     * @return static
165
     */
166
    public function data($name, $value = null)
167
    {
168
        return $this->attribute("data-{$name}", $value);
169
    }
170
171
    /**
172
     * @param \Spatie\Html\HtmlElement|string|iterable|null $children
173
     * @param callable|null $mapper
174
     *
175
     * @return static
176
     */
177 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...
178
    {
179
        if (is_null($children)) {
180
            return $this;
181
        }
182
183
        $children = $this->parseChildren($children, $mapper);
184
185
        $element = clone $this;
186
187
        $element->children = $element->children->merge($children);
188
189
        return $element;
190
    }
191
192
    /**
193
     * Alias for `addChildren`.
194
     *
195
     * @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...
196
     * @param callable|null $mapper
197
     *
198
     * @return static
199
     */
200
    public function addChild($child, $mapper = null)
201
    {
202
        return $this->addChildren($child, $mapper);
203
    }
204
205
    /**
206
     * Alias for `addChildren`.
207
     *
208
     * @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...
209
     * @param callable|null $mapper
210
     *
211
     * @return static
212
     */
213
    public function child($child, $mapper = null)
214
    {
215
        return $this->addChildren($child, $mapper);
216
    }
217
218
    /**
219
     * Alias for `addChildren`.
220
     *
221
     * @param \Spatie\Html\HtmlElement|string|iterable|null $children
222
     * @param callable|null $mapper
223
     *
224
     * @return static
225
     */
226
    public function children($children, $mapper = null)
227
    {
228
        return $this->addChildren($children, $mapper);
229
    }
230
231
    /**
232
     * Replace all children with an array of elements.
233
     *
234
     * @param \Spatie\Html\HtmlElement[] $children
235
     * @param callable|null $mapper
236
     *
237
     * @return static
238
     */
239
    public function setChildren($children, $mapper = null)
240
    {
241
        $element = clone $this;
242
243
        $element->children = new Collection();
244
245
        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...
246
    }
247
248
    /**
249
     * @param \Spatie\Html\HtmlElement|string|iterable|null $children
250
     * @param callable|null $mapper
251
     *
252
     * @return static
253
     */
254 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...
255
    {
256
        $children = $this->parseChildren($children, $mapper);
257
258
        $element = clone $this;
259
260
        $element->children = $children->merge($element->children);
261
262
        return $element;
263
    }
264
265
    /**
266
     * Alias for `prependChildren`.
267
     *
268
     * @param \Spatie\Html\HtmlElement|string|iterable|null $children
269
     * @param callable|null $mapper
270
     *
271
     * @return static
272
     */
273
    public function prependChild($children, $mapper = null)
274
    {
275
        return $this->prependChildren($children, $mapper);
276
    }
277
278
    /**
279
     * @param string|null $text
280
     *
281
     * @return static
282
     */
283
    public function text($text)
284
    {
285
        return $this->html(htmlentities($text, ENT_QUOTES, 'UTF-8', false));
286
    }
287
288
    /**
289
     * @param string|null $html
290
     *
291
     * @return static
292
     */
293
    public function html($html)
294
    {
295
        if ($this->isVoidElement()) {
296
            throw new InvalidHtml("Can't set inner contents on `{$this->tag}` because it's a void element");
297
        }
298
299
        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...
300
    }
301
302
    /**
303
     * Conditionally transform the element. Note that since elements are
304
     * immutable, you'll need to return a new instance from the callback.
305
     *
306
     * @param bool $condition
307
     * @param \Closure $callback
308
     *
309
     * @return mixed
310
     */
311
    public function if(bool $condition, \Closure $callback)
312
    {
313
        return $condition ? $callback($this) : $this;
314
    }
315
316
    /**
317
     * Conditionally transform the element. Note that since elements are
318
     * immutable, you'll need to return a new instance from the callback.
319
     *
320
     * @param bool $condition
321
     * @param \Closure $callback
322
     *
323
     * @return mixed
324
     */
325
    public function unless(bool $condition, \Closure $callback)
326
    {
327
        return $this->if(! $condition, $callback);
328
    }
329
330
    /**
331
     * Conditionally transform the element. Note that since elements are
332
     * immutable, you'll need to return a new instance from the callback.
333
     *
334
     * @param mixed $value
335
     * @param \Closure $callback
336
     *
337
     * @return mixed
338
     */
339
    public function ifNotNull($value, \Closure $callback)
340
    {
341
        return ! is_null($value) ? $callback($this) : $this;
342
    }
343
344
    /**
345
     * @return \Illuminate\Contracts\Support\Htmlable
346
     */
347
    public function open()
348
    {
349
        $tag = $this->attributes->isEmpty()
350
            ? '<'.$this->tag.'>'
351
            : "<{$this->tag} {$this->attributes->render()}>";
352
353
        $children = $this->children->map(function ($child): string {
354
            if ($child instanceof HtmlElement) {
355
                return $child->render();
356
            }
357
358
            if (is_null($child)) {
359
                return '';
360
            }
361
362
            if (is_string($child)) {
363
                return $child;
364
            }
365
366
            throw InvalidChild::childMustBeAnHtmlElementOrAString();
367
        })->implode('');
368
369
        return new HtmlString($tag.$children);
370
    }
371
372
    /**
373
     * @return \Illuminate\Contracts\Support\Htmlable
374
     */
375
    public function close()
376
    {
377
        return new HtmlString(
378
            $this->isVoidElement()
379
                ? ''
380
                : "</{$this->tag}>"
381
        );
382
    }
383
384
    /**
385
     * @return \Illuminate\Contracts\Support\Htmlable
386
     */
387
    public function render()
388
    {
389
        return new HtmlString(
390
            $this->open().$this->close()
391
        );
392
    }
393
394
    public function isVoidElement(): bool
395
    {
396
        return in_array($this->tag, [
397
            'area', 'base', 'br', 'col', 'embed', 'hr',
398
            'img', 'input', 'keygen', 'link', 'menuitem',
399
            'meta', 'param', 'source', 'track', 'wbr',
400
        ]);
401
    }
402
403
    /**
404
     * Dynamically handle calls to the class.
405
     * Check for methods finishing by If or fallback to Macroable.
406
     *
407
     * @param  string  $name
408
     * @param  array   $arguments
409
     * @return mixed
410
     *
411
     * @throws BadMethodCallException
412
     */
413
    public function __call($name, $arguments)
414
    {
415
        if (ends_with($name, $conditions = ['If', 'Unless', 'IfNotNull'])) {
0 ignored issues
show
Deprecated Code introduced by
The function ends_with() has been deprecated with message: Str::endsWith() should be used directly instead. Will be removed in Laravel 5.9.

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

Loading history...
416
            foreach ($conditions as $condition) {
417
                if (! method_exists($this, $method = str_replace($condition, '', $name))) {
418
                    continue;
419
                }
420
421
                return $this->callConditionalMethod($condition, $method, $arguments);
422
            }
423
        }
424
425
        return $this->__macro_call($name, $arguments);
426
    }
427
428
    protected function callConditionalMethod($type, $method, array $arguments)
429
    {
430
        $value = array_shift($arguments);
431
        $callback = function () use ($method, $arguments) {
432
            return $this->{$method}(...$arguments);
433
        };
434
435
        switch ($type) {
436
            case 'If':
437
                return $this->if((bool) $value, $callback);
438
            case 'Unless':
439
                return $this->unless((bool) $value, $callback);
440
            case 'IfNotNull':
441
                return $this->ifNotNull($value, $callback);
442
            default:
443
                return $this;
444
        }
445
    }
446
447
    public function __clone()
448
    {
449
        $this->attributes = clone $this->attributes;
450
        $this->children = clone $this->children;
451
    }
452
453
    public function __toString(): string
454
    {
455
        return $this->render();
456
    }
457
458
    public function toHtml(): string
459
    {
460
        return $this->render();
461
    }
462
463
    protected function parseChildren($children, $mapper = null): Collection
464
    {
465
        if ($children instanceof HtmlElement) {
466
            $children = [$children];
467
        }
468
469
        $children = Collection::make($children);
470
471
        if ($mapper) {
472
            $children = $children->map($mapper);
473
        }
474
475
        $this->guardAgainstInvalidChildren($children);
476
477
        return $children;
478
    }
479
480
    protected function guardAgainstInvalidChildren(Collection $children)
481
    {
482
        foreach ($children as $child) {
483
            if ((! $child instanceof HtmlElement) && (! is_string($child)) && (! is_null($child))) {
484
                throw InvalidChild::childMustBeAnHtmlElementOrAString();
485
            }
486
        }
487
    }
488
}
489