Graph::__construct()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 4
c 1
b 0
f 1
dl 0
loc 8
rs 10
cc 3
nc 4
nop 2
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