Passed
Pull Request — master (#1)
by Peter
07:07
created

Collection::insertBefore()   B

Complexity

Conditions 7
Paths 15

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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