Completed
Pull Request — master (#135)
by
unknown
01:28
created

BaseElement::render()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 6
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 notNull($value, \Closure $callback)
340
    {
341
        return ! is_null($value) ? $callback($this) : $this;
342
    }
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 (ends_with($name, $conditions = ['If', 'Unless', 'NotNull'])) {
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...
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 'NotNull':
442
                return $this->notNull($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