Node   F
last analyzed

Complexity

Total Complexity 68

Size/Duplication

Total Lines 459
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 120
c 2
b 0
f 0
dl 0
loc 459
rs 2.96
wmc 68

30 Methods

Rating   Name   Duplication   Size   Complexity  
A findKey() 0 9 3
A setContent() 0 23 5
A hasIntent() 0 3 1
A getNodes() 0 11 3
A findAllShallow() 0 21 4
A offsetUnset() 0 3 1
A forceGetNodes() 0 13 3
B isMatch() 0 17 7
A add() 0 7 1
A offsetExists() 0 3 1
A count() 0 3 1
A check() 0 14 4
A next() 0 3 1
A setIntent() 0 5 1
A current() 0 3 1
A setTranslator() 0 9 2
A getTranslator() 0 3 1
A getIntents() 0 3 1
A offsetGet() 0 3 1
A replace() 0 18 5
A __toString() 0 15 5
A rewind() 0 3 1
A valid() 0 3 1
A findAll() 0 3 1
A find() 0 14 4
A addIntent() 0 5 1
A key() 0 3 1
A __construct() 0 7 2
A getExtendedNodes() 0 3 1
A offsetSet() 0 10 4

How to fix   Complexity   

Complex Class

Complex classes like Node 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.

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

1
<?php
2
3
declare(strict_types=1);
4
5
namespace AbterPhp\Framework\Html;
6
7
use AbterPhp\Framework\Html\Helper\Collection;
8
use AbterPhp\Framework\I18n\ITranslator;
9
use ArrayAccess;
10
use Closure;
11
use Countable;
12
use Iterator;
13
14
/**
15
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
16
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
17
 */
18
class Node implements INode, Iterator, ArrayAccess, Countable
19
{
20
    protected const ERROR_INVALID_OFFSET = 'Offset must be a positive integer and not larger than number of items'; // phpcs:ignore
21
22
    protected const CONTENT_TYPE = '';
23
24
    protected const SEPARATOR = '';
25
26
    /** @var array<string|INode> */
27
    protected array $content = [];
28
29
    /** @var int */
30
    protected int $position = 0;
31
32
    /**
33
     * Intents are a way to achieve frontend-framework independence.
34
     * A button with an intention of "primary-action" can then
35
     * receive a "btn btn-primary" class from a Bootstrap-based
36
     * decorator
37
     *
38
     * @var string[]
39
     */
40
    protected array $intents = [];
41
42
    protected ?ITranslator $translator = null;
43
44
    /**
45
     * Node constructor.
46
     *
47
     * @param array<string|INode>|string|INode|null $content
48
     * @param string                                ...$intents
49
     */
50
    public function __construct($content = null, string ...$intents)
51
    {
52
        if ($content !== null) {
53
            $this->setContent($content);
54
        }
55
56
        $this->setIntent(...$intents);
57
    }
58
59
    /**
60
     * @param array<string|IStringer>|string|IStringer|null $content
61
     *
62
     * @return $this
63
     */
64
    public function setContent($content): self
65
    {
66
        $this->check($content);
67
68
        $this->content = [];
69
70
        if (null === $content) {
71
            return $this;
72
        }
73
74
        if (!is_array($content)) {
75
            $content = [$content];
76
        }
77
78
        foreach ($content as $item) {
79
            if ($item instanceof INode) {
80
                $this->content[] = $item;
81
            } else {
82
                $this->content[] = (string)$item;
83
            }
84
        }
85
86
        return $this;
87
    }
88
89
    /**
90
     * @return INode[]
91
     */
92
    public function getNodes(): array
93
    {
94
        $result = [];
95
96
        foreach ($this->content as $item) {
97
            if ($item instanceof INode) {
98
                $result[] = $item;
99
            }
100
        }
101
102
        return $result;
103
    }
104
105
    /**
106
     * @return INode[]
107
     */
108
    public function getExtendedNodes(): array
109
    {
110
        return $this->getNodes();
111
    }
112
113
    /**
114
     * @return INode[]
115
     */
116
    public function forceGetNodes(): array
117
    {
118
        $result = [];
119
120
        foreach ($this->content as $item) {
121
            if ($item instanceof INode) {
122
                $result[] = $item;
123
            } else {
124
                $result[] = new Node($item);
125
            }
126
        }
127
128
        return $result;
129
    }
130
131
    /**
132
     * @param string $intent
133
     *
134
     * @return bool
135
     */
136
    public function hasIntent(string $intent): bool
137
    {
138
        return in_array($intent, $this->intents, true);
139
    }
140
141
    /**
142
     * @return string[]
143
     */
144
    public function getIntents(): array
145
    {
146
        return $this->intents;
147
    }
148
149
    /**
150
     * @param string ...$intent
151
     *
152
     * @return $this
153
     */
154
    public function setIntent(string ...$intent): self
155
    {
156
        $this->intents = $intent;
157
158
        return $this;
159
    }
160
161
    /**
162
     * @param string ...$intent
163
     *
164
     * @return $this
165
     */
166
    public function addIntent(string ...$intent): self
167
    {
168
        $this->intents = array_merge($this->intents, $intent);
169
170
        return $this;
171
    }
172
173
    /**
174
     * @param ITranslator|null $translator
175
     *
176
     * @return $this
177
     */
178
    public function setTranslator(?ITranslator $translator): self
179
    {
180
        $this->translator = $translator;
181
182
        foreach ($this->getExtendedNodes() as $node) {
183
            $node->setTranslator($translator);
184
        }
185
186
        return $this;
187
    }
188
189
    /**
190
     * @return ITranslator|null
191
     */
192
    public function getTranslator(): ?ITranslator
193
    {
194
        return $this->translator;
195
    }
196
197
    /**
198
     * Checks if the current component matches the arguments provided
199
     *
200
     * @param string|null  $className
201
     * @param Closure|null $matcher
202
     * @param string       ...$intents
203
     *
204
     * @return bool
205
     */
206
    public function isMatch(?string $className = null, ?Closure $matcher = null, string ...$intents): bool
207
    {
208
        if ($className && !($this instanceof $className)) {
209
            return false;
210
        }
211
212
        if ($matcher && !$matcher($this)) {
213
            return false;
214
        }
215
216
        foreach ($intents as $intent) {
217
            if (!in_array($intent, $this->intents, true)) {
218
                return false;
219
            }
220
        }
221
222
        return true;
223
    }
224
225
    /**
226
     * @param string|null  $className
227
     * @param Closure|null $matcher
228
     * @param string       ...$intents
229
     *
230
     * @return INode|null
231
     */
232
    public function find(?string $className = null, ?Closure $matcher = null, string ...$intents): ?INode
233
    {
234
        if ($this->isMatch($className, $matcher, ...$intents)) {
235
            return $this;
236
        }
237
238
        foreach ($this->getNodes() as $node) {
239
            $found = $node->find($className, $matcher, ...$intents);
240
            if ($found) {
241
                return $found;
242
            }
243
        }
244
245
        return null;
246
    }
247
248
    /**
249
     * Finds all sub-nodes which match a certain criteria
250
     *
251
     * @param string|null  $className
252
     * @param Closure|null $matcher
253
     * @param string       ...$intents
254
     *
255
     * @return INode[]
256
     */
257
    public function findAll(?string $className = null, ?Closure $matcher = null, string ...$intents): array
258
    {
259
        return $this->findAllShallow(-1, $className, $matcher, ...$intents);
260
    }
261
262
    /**
263
     * @param int          $maxDepth
264
     * @param string|null  $className
265
     * @param Closure|null $matcher
266
     * @param string       ...$intents
267
     *
268
     * @return INode[]
269
     */
270
    public function findAllShallow(
271
        int $maxDepth,
272
        ?string $className = null,
273
        ?Closure $matcher = null,
274
        string ...$intents
275
    ): array {
276
        $result = [];
277
278
        if ($this->isMatch($className, $matcher, ...$intents)) {
279
            $result[] = $this;
280
        }
281
282
        if ($maxDepth === 0) {
283
            return $result;
284
        }
285
286
        foreach ($this->getNodes() as $node) {
287
            $result = array_merge($result, $node->findAllShallow($maxDepth - 1, $className, $matcher, ...$intents));
288
        }
289
290
        return $result;
291
    }
292
293
    /**
294
     * @param INode|string $itemToFind
295
     *
296
     * @return int|null
297
     */
298
    public function findKey($itemToFind): ?int
299
    {
300
        foreach ($this->content as $key => $item) {
301
            if ($item === $itemToFind) {
302
                return $key;
303
            }
304
        }
305
306
        return null;
307
    }
308
309
    /**
310
     * Replaces a given node with a number of nodes
311
     * It will also call the children to execute the same operation if the node was not found
312
     *
313
     * @param INode $itemToFind
314
     * @param INode ...$items
315
     *
316
     * @return bool
317
     */
318
    public function replace(INode $itemToFind, INode ...$items): bool
319
    {
320
        $this->check($items);
321
322
        $key = $this->findKey($itemToFind);
323
        if ($key !== null) {
324
            array_splice($this->content, $key, 1, $items);
325
326
            return true;
327
        }
328
329
        foreach ($this->content as $item) {
330
            if ($item instanceof INode && $item->replace($itemToFind, ...$items)) {
331
                return true;
332
            }
333
        }
334
335
        return false;
336
    }
337
338
    /**
339
     * @param INode ...$items
340
     *
341
     * @return $this
342
     */
343
    public function add(INode ...$items): self
344
    {
345
        $this->check($items);
346
347
        $this->content = array_merge($this->content, $items);
348
349
        return $this;
350
    }
351
352
    /**
353
     * @param array<string|INode>|string|INode|null $content
354
     */
355
    protected function check($content = null)
356
    {
357
        if ($content === null) {
358
            return;
359
        }
360
361
        if (!is_array($content)) {
362
            $content = [$content];
363
        }
364
365
        if (!static::CONTENT_TYPE) {
366
            assert(Collection::allNodes($content));
367
        } else {
368
            assert(Collection::allInstanceOf($content, static::CONTENT_TYPE));
369
        }
370
    }
371
372
    /**
373
     * @return string
374
     */
375
    public function __toString(): string
376
    {
377
        $items = [];
378
        foreach ($this->content as $item) {
379
            if (is_scalar($item) && $this->getTranslator()) {
380
                $i = $this->getTranslator()->translate($item);
381
            } else {
382
                $i = (string)$item;
383
            }
384
            if ($i !== '') {
385
                $items[] = $i;
386
            }
387
        }
388
389
        return join(static::SEPARATOR, $items);
390
    }
391
392
    /**
393
     * @param int|null $offset
394
     * @param INode    $value
395
     */
396
    public function offsetSet($offset, $value): void
397
    {
398
        assert(Collection::allInstanceOf([$value], static::CONTENT_TYPE));
399
400
        if (is_null($offset)) {
401
            $this->content[] = $value;
402
        } elseif ($offset < 0 || $offset > count($this->content)) {
403
            throw new \InvalidArgumentException(static::ERROR_INVALID_OFFSET);
404
        } else {
405
            $this->content[$offset] = $value;
406
        }
407
    }
408
409
    /**
410
     * @param int $offset
411
     *
412
     * @return bool
413
     */
414
    public function offsetExists($offset): bool
415
    {
416
        return isset($this->content[$offset]);
417
    }
418
419
    /**
420
     * @param int $offset
421
     */
422
    public function offsetUnset($offset): void
423
    {
424
        unset($this->content[$offset]);
425
    }
426
427
    /**
428
     * @param int $offset
429
     *
430
     * @return INode|null
431
     */
432
    public function offsetGet($offset): ?INode
433
    {
434
        return $this->content[$offset] ?? null;
435
    }
436
437
    /**
438
     * @return int
439
     */
440
    public function count(): int
441
    {
442
        return count($this->content);
443
    }
444
445
    public function rewind(): void
446
    {
447
        $this->position = 0;
448
    }
449
450
    /**
451
     * @return INode
452
     */
453
    public function current(): INode
454
    {
455
        return $this->content[$this->position];
456
    }
457
458
    /**
459
     * @return int
460
     */
461
    public function key(): int
462
    {
463
        return $this->position;
464
    }
465
466
    public function next(): void
467
    {
468
        ++$this->position;
469
    }
470
471
    /**
472
     * @return bool
473
     */
474
    public function valid(): bool
475
    {
476
        return isset($this->content[$this->position]);
477
    }
478
}
479