Completed
Push — master ( b9f630...d280b1 )
by Sebastian
02:43
created

BaseElement   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 410
Duplicated Lines 5.85 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
dl 24
loc 410
rs 8.439
c 0
b 0
f 0
wmc 47
lcom 1
cbo 6

31 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 2
A create() 0 4 1
A attribute() 0 8 1
A attributeIf() 0 6 2
A attributes() 0 8 1
A forgetAttribute() 0 8 1
A getAttribute() 0 4 1
A hasAttribute() 0 4 1
A class() 0 4 1
A addClass() 0 8 1
A id() 0 4 1
A style() 0 10 2
A addChildren() 14 14 2
A addChild() 0 4 1
A child() 0 4 1
A children() 0 4 1
A setChildren() 0 8 1
A prependChildren() 10 10 1
A prependChild() 0 4 1
A text() 0 4 1
A html() 0 12 2
A if() 0 8 2
B open() 0 24 5
A close() 0 8 2
A render() 0 6 1
A isVoidElement() 0 8 1
A __clone() 0 5 1
A __toString() 0 4 1
A toHtml() 0 4 1
A parseChildren() 0 18 3
A guardAgainstInvalidChildren() 0 8 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like BaseElement 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 BaseElement, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Spatie\Html;
4
5
use Illuminate\Support\Collection;
6
use Illuminate\Support\HtmlString;
7
use Spatie\Html\Exceptions\MissingTag;
8
use Spatie\Html\Exceptions\InvalidHtml;
9
use Spatie\Html\Exceptions\InvalidChild;
10
use Illuminate\Contracts\Support\Htmlable;
11
12
abstract class BaseElement implements Htmlable, HtmlElement
13
{
14
    /** @var string */
15
    protected $tag;
16
17
    /** @var \Spatie\Html\Attributes */
18
    protected $attributes;
19
20
    /** @var \Illuminate\Support\Collection */
21
    protected $children;
22
23
    public function __construct()
24
    {
25
        if (empty($this->tag)) {
26
            throw MissingTag::onClass(static::class);
27
        }
28
29
        $this->attributes = new Attributes();
30
        $this->children = new Collection();
31
    }
32
33
    public static function create()
34
    {
35
        return new static();
36
    }
37
38
    /**
39
     * @param string $attribute
40
     * @param string|null $value
41
     *
42
     * @return static
43
     */
44
    public function attribute($attribute, $value = null)
45
    {
46
        $element = clone $this;
47
48
        $element->attributes->setAttribute($attribute, (string) $value);
49
50
        return $element;
51
    }
52
53
    /**
54
     * @param bool $condition
55
     * @param string $attribute
56
     * @param string|null $value
57
     *
58
     * @return static
59
     */
60
    public function attributeIf($condition, $attribute, $value = null)
61
    {
62
        return $condition ?
63
            $this->attribute($attribute, $value) :
64
            $this;
65
    }
66
67
    /**
68
     * @param iterable $attributes
69
     *
70
     * @return static
71
     */
72
    public function attributes($attributes)
73
    {
74
        $element = clone $this;
75
76
        $element->attributes->setAttributes($attributes);
77
78
        return $element;
79
    }
80
81
    /**
82
     * @param string $attribute
83
     *
84
     * @return static
85
     */
86
    public function forgetAttribute($attribute)
87
    {
88
        $element = clone $this;
89
90
        $element->attributes->forgetAttribute($attribute);
91
92
        return $element;
93
    }
94
95
    /**
96
     * @param string $attribute
97
     * @param mixed $fallback
98
     *
99
     * @return mixed
100
     */
101
    public function getAttribute($attribute, $fallback = null)
102
    {
103
        return $this->attributes->getAttribute($attribute, $fallback);
104
    }
105
106
    /**
107
     * @param string $attribute
108
     *
109
     * @return bool
110
     */
111
    public function hasAttribute($attribute)
112
    {
113
        return $this->attributes->hasAttribute($attribute);
114
    }
115
116
    /**
117
     * @param iterable|string $class
118
     *
119
     * @return static
120
     */
121
    public function class($class)
0 ignored issues
show
Coding Style introduced by
Possible parse error: non-abstract method defined as abstract
Loading history...
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
122
    {
123
        return $this->addClass($class);
124
    }
125
126
    /**
127
     * Alias for `class`.
128
     *
129
     * @param iterable|string $class
130
     *
131
     * @return static
132
     */
133
    public function addClass($class)
134
    {
135
        $element = clone $this;
136
137
        $element->attributes->addClass($class);
138
139
        return $element;
140
    }
141
142
    /**
143
     * @param string $id
144
     *
145
     * @return static
146
     */
147
    public function id($id)
148
    {
149
        return $this->attribute('id', $id);
150
    }
151
152
    /**
153
     * @param array|string|null $style
154
     *
155
     * @return static
156
     */
157
    public function style($style)
158
    {
159
        if (is_array($style)) {
160
            $style = implode('; ', array_map(function ($value, $attribute) {
161
                return "{$attribute}: {$value}";
162
            }, $style, array_keys($style)));
163
        }
164
165
        return $this->attribute('style', $style);
166
    }
167
168
    /**
169
     * @param \Spatie\Html\HtmlElement|string|iterable|null $children
170
     * @param callable|null $mapper
171
     *
172
     * @return static
173
     */
174 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...
175
    {
176
        if (is_null($children)) {
177
            return $this;
178
        }
179
180
        $children = $this->parseChildren($children, $mapper);
181
182
        $element = clone $this;
183
184
        $element->children = $element->children->merge($children);
185
186
        return $element;
187
    }
188
189
    /**
190
     * Alias for `addChildren`.
191
     *
192
     * @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...
193
     * @param callable|null $mapper
194
     *
195
     * @return static
196
     */
197
    public function addChild($child, $mapper = null)
198
    {
199
        return $this->addChildren($child, $mapper);
200
    }
201
202
    /**
203
     * Alias for `addChildren`.
204
     *
205
     * @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...
206
     * @param callable|null $mapper
207
     *
208
     * @return static
209
     */
210
    public function child($child, $mapper = null)
211
    {
212
        return $this->addChildren($child, $mapper);
213
    }
214
215
    /**
216
     * Alias for `addChildren`.
217
     *
218
     * @param \Spatie\Html\HtmlElement|string|iterable|null $children
219
     * @param callable|null $mapper
220
     *
221
     * @return static
222
     */
223
    public function children($children, $mapper = null)
224
    {
225
        return $this->addChildren($children, $mapper);
226
    }
227
228
    /**
229
     * Replace all children with an array of elements.
230
     *
231
     * @param \Spatie\Html\HtmlElement[] $children
232
     * @param callable|null $mapper
233
     *
234
     * @return static
235
     */
236
    public function setChildren($children, $mapper = null)
237
    {
238
        $element = clone $this;
239
240
        $element->children = new Collection();
241
242
        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...
243
    }
244
245
    /**
246
     * @param \Spatie\Html\HtmlElement|string|iterable|null $children
247
     * @param callable|null $mapper
248
     *
249
     * @return static
250
     */
251 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...
252
    {
253
        $children = $this->parseChildren($children, $mapper);
254
255
        $element = clone $this;
256
257
        $element->children = $children->merge($element->children);
258
259
        return $element;
260
    }
261
262
    /**
263
     * Alias for `prependChildren`.
264
     *
265
     * @param \Spatie\Html\HtmlElement|string|iterable|null $children
266
     * @param callable|null $mapper
267
     *
268
     * @return static
269
     */
270
    public function prependChild($children, $mapper = null)
271
    {
272
        return $this->prependChildren($children, $mapper);
273
    }
274
275
    /**
276
     * @param string|null $text
277
     *
278
     * @return static
279
     */
280
    public function text($text)
281
    {
282
        return $this->html(htmlentities($text, ENT_QUOTES, 'UTF-8', false));
283
    }
284
285
    /**
286
     * @param string|null $html
287
     *
288
     * @return static
289
     */
290
    public function html($html)
291
    {
292
        if ($this->isVoidElement()) {
293
            throw new InvalidHtml("Can't set inner contents on `{$this->tag}` because it's a void element");
294
        }
295
296
        $element = clone $this;
297
298
        $element->children = new Collection([$html]);
299
300
        return $element;
301
    }
302
303
    /**
304
     * Condintionally 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 callable $callback
309
     */
310
    public function if(bool $condition, callable $callback)
0 ignored issues
show
Coding Style introduced by
Possible parse error: non-abstract method defined as abstract
Loading history...
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
311
    {
312
        if ($condition) {
313
            return $callback($this);
314
        }
315
316
        return $this;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $this.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
317
    }
318
319
    /**
320
     * @return \Illuminate\Contracts\Support\Htmlable
321
     */
322
    public function open()
323
    {
324
        $tag = $this->attributes->isEmpty()
325
            ? '<'.$this->tag.'>'
326
            : "<{$this->tag} {$this->attributes->render()}>";
327
328
        $children = $this->children->map(function ($child): string {
329
            if ($child instanceof HtmlElement) {
330
                return $child->render();
331
            }
332
333
            if (is_null($child)) {
334
                return '';
335
            }
336
337
            if (is_string($child)) {
338
                return $child;
339
            }
340
341
            throw InvalidChild::childMustBeAnHtmlElementOrAString();
342
        })->implode('');
343
344
        return new HtmlString($tag.$children);
345
    }
346
347
    /**
348
     * @return \Illuminate\Contracts\Support\Htmlable
349
     */
350
    public function close()
351
    {
352
        return new HtmlString(
353
            $this->isVoidElement()
354
                ? ''
355
                : "</{$this->tag}>"
356
        );
357
    }
358
359
    /**
360
     * @return \Illuminate\Contracts\Support\Htmlable
361
     */
362
    public function render()
363
    {
364
        return new HtmlString(
365
            $this->open().$this->close()
366
        );
367
    }
368
369
    public function isVoidElement(): bool
370
    {
371
        return in_array($this->tag, [
372
            'area', 'base', 'br', 'col', 'embed', 'hr',
373
            'img', 'input', 'keygen', 'link', 'menuitem',
374
            'meta', 'param', 'source', 'track', 'wbr',
375
        ]);
376
    }
377
378
    public function __clone()
379
    {
380
        $this->attributes = clone $this->attributes;
381
        $this->children = clone $this->children;
382
    }
383
384
    public function __toString(): string
385
    {
386
        return $this->render();
387
    }
388
389
    public function toHtml(): string
390
    {
391
        return $this->render();
392
    }
393
394
    protected function parseChildren($children, $mapper = null): Collection
395
    {
396
        if ($children instanceof HtmlElement) {
397
            $children = [$children];
398
        }
399
400
        $children = Collection::make($children);
401
402
        if ($mapper) {
403
            $children = $children->map($mapper);
404
        }
405
406
        $children = $children->filter();
407
408
        $this->guardAgainstInvalidChildren($children);
409
410
        return $children;
411
    }
412
413
    protected function guardAgainstInvalidChildren(Collection $children)
414
    {
415
        foreach ($children as $child) {
416
            if ((! $child instanceof HtmlElement) && (! is_string($child))) {
417
                throw InvalidChild::childMustBeAnHtmlElementOrAString();
418
            }
419
        }
420
    }
421
}
422