Graph::traverseRecursive()   B
last analyzed

Complexity

Conditions 11
Paths 11

Size

Total Lines 57
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 32
c 1
b 1
f 0
dl 0
loc 57
rs 7.3166
cc 11
nc 11
nop 10

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace Smoren\Containers\Structs;
4
5
use ArrayIterator;
6
use Countable;
7
use IteratorAggregate;
8
use Smoren\Containers\Exceptions\GraphException;
9
10
class Graph implements Countable, IteratorAggregate
11
{
12
    /**
13
     * @var GraphItem[]
14
     */
15
    protected array $itemsMap = [];
16
17
    /**
18
     * Graph constructor.
19
     * @param array $dataMap map of items data by ID ([ID => data, ...])
20
     * @param array $links items links ([[leftItemId, rightItemId, linkType], ...])
21
     * @throws GraphException
22
     */
23
    public function __construct(array $dataMap = [], array $links = [])
24
    {
25
        foreach($dataMap as $id => $data) {
26
            $this->insert($id, $data);
27
        }
28
29
        foreach($links as $link) {
30
            $this->link(...$link);
0 ignored issues
show
Bug introduced by
The call to Smoren\Containers\Structs\Graph::link() has too few arguments starting with rhsId. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

30
            $this->/** @scrutinizer ignore-call */ 
31
                   link(...$link);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
31
        }
32
    }
33
34
    /**
35
     * Inserts item into graph
36
     * @param string $id item ID
37
     * @param mixed $data item data
38
     * @return GraphItem
39
     * @throws GraphException
40
     */
41
    public function insert(string $id, $data): GraphItem
42
    {
43
        $this->checkNotExist($id);
44
45
        $item = new GraphItem($id, $data);
46
        $this->itemsMap[$id] = $item;
47
48
        return $item;
49
    }
50
51
    /**
52
     * Deletes item from graph
53
     * @param string $id item ID
54
     * @return mixed
55
     * @throws GraphException
56
     */
57
    public function delete(string $id)
58
    {
59
        $item = $this->getItem($id);
60
61
        foreach($item->getPrevItems() as $prevItem) {
62
            $prevItem->deleteNextItem($id);
63
        }
64
65
        foreach($item->getNextItems() as $nextItem) {
66
            $nextItem->deletePrevItem($id);
67
        }
68
69
        unset($this->itemsMap[$id]);
70
71
        return $item->getData();
72
    }
73
74
    /**
75
     * Makes link of 2 items
76
     * @param string $lhsId left item ID
77
     * @param string $rhsId right item ID
78
     * @return $this
79
     * @throws GraphException
80
     */
81
    public function link(string $lhsId, string $rhsId, string $type = 'default'): self
82
    {
83
        $lhs = $this->get($lhsId);
84
        $rhs = $this->get($rhsId);
85
86
        $lhs->addNextItem($rhs, $type);
87
        $rhs->addPrevItem($lhs, $type);
88
89
        return $this;
90
    }
91
92
    /**
93
     * Deletes link of 2 items
94
     * @param string $lhsId left item ID
95
     * @param string $rhsId right item ID
96
     * @param string|null $type link type
97
     * @return $this
98
     * @throws GraphException
99
     */
100
    public function unlink(string $lhsId, string $rhsId, ?string $type = null): self
101
    {
102
        $lhs = $this->get($lhsId);
103
        $rhs = $this->get($rhsId);
104
105
        $lhs->deleteNextItem($rhs->getId(), $type);
106
        $rhs->deletePrevItem($lhs->getId(), $type);
107
108
        return $this;
109
    }
110
111
    /**
112
     * Returns item data by ID
113
     * @param string $id item ID
114
     * @param mixed $default default value if item is not found
115
     * @return mixed data value of item
116
     * @throws GraphException
117
     */
118
    public function get(string $id, $default = null)
119
    {
120
        try {
121
            $this->checkExist($id);
122
        } catch(GraphException $e) {
123
            if($default !== null) {
124
                return $default;
125
            } else {
126
                throw $e;
127
            }
128
        }
129
130
        return $this->itemsMap[$id];
131
    }
132
133
    /**
134
     * Returns item by ID
135
     * @param string $id item ID
136
     * @param mixed $default default value if item is not found
137
     * @return GraphItem data item
138
     * @throws GraphException
139
     */
140
    public function getItem(string $id, $default = null): GraphItem
141
    {
142
        try {
143
            $this->checkExist($id);
144
        } catch(GraphException $e) {
145
            if($default !== null) {
146
                return $default;
147
            } else {
148
                throw $e;
149
            }
150
        }
151
152
        return $this->itemsMap[$id];
153
    }
154
155
    /**
156
     * Get all traverse paths from item to backward
157
     * @param string $itemId item ID
158
     * @param array|null $typesOnly list of types to use in traverse
159
     * @param array|null $typesExclude list of types not to use in traverse
160
     * @param int|null $maxPathLength max path length
161
     * @param bool $stopOnLoop stop on loop
162
     * @param callable|null $callback callback for every traverse link
163
     * @return GraphTraversePath[]
164
     * @throws GraphException
165
     */
166
    public function traverseBackward(
167
        string $itemId,
168
        ?array $typesOnly = null,
169
        ?array $typesExclude = null,
170
        ?int $maxPathLength = null,
171
        bool $stopOnLoop = true,
172
        ?callable $callback = null
173
    ): array {
174
        return $this->makeTraversePathCollection(
175
            $this->traverseRecursive(
176
                'getPrevItemsMap',
177
                $itemId,
178
                $typesOnly,
179
                $typesExclude,
180
                $callback,
181
                $maxPathLength,
182
                $stopOnLoop
183
            )
184
        );
185
    }
186
187
    /**
188
     * Get all traverse paths from item to forward
189
     * @param string $itemId item ID
190
     * @param array|null $typesOnly list of types to use in traverse
191
     * @param array|null $typesExclude list of types not to use in traverse
192
     * @param int|null $maxPathLength max path length
193
     * @param bool $stopOnLoop stop on loop
194
     * @param callable|null $callback callback for every traverse link
195
     * @return GraphTraversePath[]
196
     * @throws GraphException
197
     */
198
    public function traverseForward(
199
        string $itemId,
200
        ?array $typesOnly = null,
201
        ?array $typesExclude = null,
202
        ?int $maxPathLength = null,
203
        bool $stopOnLoop = true,
204
        ?callable $callback = null
205
    ): array {
206
        return $this->makeTraversePathCollection(
207
            $this->traverseRecursive(
208
                'getNextItemsMap',
209
                $itemId,
210
                $typesOnly,
211
                $typesExclude,
212
                $callback,
213
                $maxPathLength,
214
                $stopOnLoop
215
            )
216
        );
217
    }
218
219
    /**
220
     * Returns true if item with such ID exists in graph
221
     * @param string $id item ID
222
     * @return bool
223
     */
224
    public function exist(string $id): bool
225
    {
226
        return isset($this->itemsMap[$id]);
227
    }
228
229
    /**
230
     * Checks if element with such ID exists
231
     * @param string $id element ID
232
     * @return $this
233
     * @throws GraphException
234
     */
235
    public function checkExist(string $id): self
236
    {
237
        if(!$this->exist($id)) {
238
            throw new GraphException(
239
                "ID '{$id}' not exists",
240
                GraphException::STATUS_ID_NOT_EXIST
241
            );
242
        }
243
        return $this;
244
    }
245
246
    /**
247
     * Checks if element with such ID does not exist
248
     * @param string $id element ID
249
     * @return $this
250
     * @throws GraphException
251
     */
252
    public function checkNotExist(string $id): self
253
    {
254
        if($this->exist($id)) {
255
            throw new GraphException(
256
                "ID '{$id}' exists",
257
                GraphException::STATUS_ID_EXIST
258
            );
259
        }
260
        return $this;
261
    }
262
263
    /**
264
     * Clears graph
265
     * @return $this
266
     */
267
    public function clear(): self
268
    {
269
        $this->itemsMap = [];
270
        return $this;
271
    }
272
273
    /**
274
     * Represents graph as array
275
     * @return array
276
     */
277
    public function toArray(): array
278
    {
279
        $result = [];
280
281
        foreach($this->itemsMap as $item) {
282
            $result[] = $item->toArray();
283
        }
284
285
        return $result;
286
    }
287
288
    /**
289
     * @inheritDoc
290
     * @return int
291
     */
292
    public function count(): int
293
    {
294
        return count($this->itemsMap);
295
    }
296
297
    /**
298
     * Makes list of GraphTraversePaths by result of recursive traverse
299
     * @param GraphLink[][] $traverseData input data
300
     * @return GraphTraversePath[]
301
     */
302
    protected function makeTraversePathCollection(array $traverseData): array
303
    {
304
        $result = [];
305
306
        foreach($traverseData as $links) {
307
            $result[] = new GraphTraversePath($links);
308
        }
309
310
        return $result;
311
    }
312
313
    /**
314
     * Recursive method to find all traverse paths from some item to all dead ends
315
     * @param string $getLinkedItemsMethodName method name for getting linked items
316
     * @param string $itemId id of item to traverse from
317
     * @param array|null $typesOnly list of types to use in traverse
318
     * @param array|null $typesExclude list of types not to use in traverse
319
     * @param callable|null $callback callback for every traverse link
320
     * @param int|null $maxPathLength max path length
321
     * @param GraphItem|null $relatedItem related item from previous recursive iteration
322
     * @param string|null $type link type with related item
323
     * @param array $currentPath current state of traversed path
324
     * @return GraphLink[][]
325
     * @throws GraphException
326
     */
327
    protected function traverseRecursive(
328
        string $getLinkedItemsMethodName,
329
        string $itemId,
330
        ?array $typesOnly = null,
331
        ?array $typesExclude = null,
332
        ?callable $callback = null,
333
        ?int $maxPathLength = null,
334
        bool $stopOnLoop = true,
335
        GraphItem $relatedItem = null,
336
        ?string $type = null,
337
        array $currentPath = []
338
    ): array {
339
        $paths = [];
340
        $item = $this->getItem($itemId);
341
        $prevItemMap = $item->$getLinkedItemsMethodName($typesOnly, $typesExclude);
342
343
        if($relatedItem !== null) {
344
            $link = new GraphLink($relatedItem, $item, $type);
0 ignored issues
show
Bug introduced by
It seems like $type can also be of type null; however, parameter $type of Smoren\Containers\Structs\GraphLink::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

344
            $link = new GraphLink($relatedItem, $item, /** @scrutinizer ignore-type */ $type);
Loading history...
345
346
            if($callback !== null) {
347
                $callback($link, $currentPath);
348
            }
349
350
            if($stopOnLoop && isset($currentPath[$item->getId()])) {
351
                $currentPath[] = $link;
352
                $paths[] = array_values($currentPath);
353
                return $paths;
354
            } else {
355
                $currentPath[$relatedItem->getId()] = $link;
356
            }
357
        }
358
359
        if(count($prevItemMap) && ($maxPathLength === null || count($currentPath) < $maxPathLength-1)) {
360
            foreach($prevItemMap as $type => $itemMap) {
361
                foreach($itemMap as $itemId) {
362
                    $paths = array_merge(
363
                        $paths,
364
                        $this->traverseRecursive(
365
                            $getLinkedItemsMethodName,
366
                            $itemId,
367
                            $typesOnly,
368
                            $typesExclude,
369
                            $callback,
370
                            $maxPathLength,
371
                            $stopOnLoop,
372
                            $item,
373
                            $type,
374
                            $currentPath
375
                        )
376
                    );
377
                }
378
            }
379
        } elseif(count($currentPath)) {
380
            $paths[] = array_values($currentPath);
381
        }
382
383
        return $paths;
384
    }
385
386
    /**
387
     * @inheritDoc
388
     * @return ArrayIterator
389
     */
390
    public function getIterator(): ArrayIterator
391
    {
392
        return new ArrayIterator($this->itemsMap);
393
    }
394
}
395