Linker   A
last analyzed

Complexity

Total Complexity 38

Size/Duplication

Total Lines 243
Duplicated Lines 0 %

Importance

Changes 10
Bugs 1 Features 1
Metric Value
eloc 75
c 10
b 1
f 1
dl 0
loc 243
rs 9.36
wmc 38

13 Methods

Rating   Name   Duplication   Size   Complexity  
A getResponseBody() 0 6 2
A invokeRecursive() 0 15 3
A annotationRel() 0 18 3
A isMultiColumnList() 0 3 2
A invoke() 0 5 1
A __construct() 0 4 1
A isSingleColumnList() 0 3 2
A isMultiColumnMultiRowList() 0 13 4
A nextLink() 0 20 5
A annotationCrawl() 0 13 4
A annotationLink() 0 15 3
A crawl() 0 21 5
A isList() 0 14 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BEAR\Resource;
6
7
use BEAR\Resource\Annotation\Link;
8
use BEAR\Resource\Exception\LinkQueryException;
9
use BEAR\Resource\Exception\LinkRelException;
10
use BEAR\Resource\Exception\MethodException;
11
use BEAR\Resource\Exception\UriException;
12
use Ray\Aop\ReflectionMethod;
13
14
use function array_filter;
15
use function array_key_exists;
16
use function array_keys;
17
use function array_pop;
18
use function assert;
19
use function count;
20
use function is_array;
21
use function ucfirst;
22
use function uri_template;
23
24
/** @SuppressWarnings(PHPMD.CouplingBetweenObjects) */
25
final class Linker implements LinkerInterface
26
{
27
    /**
28
     * memory cache for linker
29
     *
30
     * @var array<string, mixed>
31
     */
32
    private array $cache = [];
33
34
    public function __construct(
35
        private readonly InvokerInterface $invoker,
36
        private readonly FactoryInterface $factory,
37
    ) {
38
    }
39
40
    /**
41
     * {@inheritDoc}
42
     */
43
    public function invoke(AbstractRequest $request)
44
    {
45
        $this->cache = [];
46
47
        return $this->invokeRecursive($request);
48
    }
49
50
    /**
51
     * @throws LinkQueryException
52
     * @throws LinkRelException
53
     */
54
    private function invokeRecursive(AbstractRequest $request): ResourceObject
55
    {
56
        $this->invoker->invoke($request);
57
        $current = clone $request->resourceObject;
58
        if ($current->code >= Code::BAD_REQUEST) {
59
            return $current;
60
        }
61
62
        foreach ($request->links as $link) {
63
            /** @var array<mixed> $nextBody */
64
            $nextBody = $this->annotationLink($link, $current, $request)->body;
65
            $current = $this->nextLink($link, $current, $nextBody);
66
        }
67
68
        return $current;
69
    }
70
71
    /**
72
     * How next linked resource treated (add ? replace ?)
73
     */
74
    private function nextLink(LinkType $link, ResourceObject $ro, mixed $nextResource): ResourceObject
75
    {
76
        /** @var array<mixed> $nextBody */
77
        $nextBody = $nextResource instanceof ResourceObject ? $nextResource->body : $nextResource;
78
79
        if ($link->type === LinkType::SELF_LINK) {
80
            $ro->body = $nextBody;
81
82
            return $ro;
83
        }
84
85
        if ($link->type === LinkType::NEW_LINK) {
86
            assert(is_array($ro->body) || $ro->body === null);
87
            $ro->body[$link->key] = $nextBody;
88
89
            return $ro;
90
        }
91
92
        // crawl
93
        return $ro;
94
    }
95
96
    /**
97
     * Annotation link
98
     *
99
     * @throws MethodException
100
     * @throws LinkRelException
101
     * @throws Exception\LinkQueryException
102
     */
103
    private function annotationLink(LinkType $link, ResourceObject $current, AbstractRequest $request): ResourceObject
104
    {
105
        if (! is_array($current->body)) {
106
            throw new Exception\LinkQueryException('Only array is allowed for link in ' . $current::class, 500);
107
        }
108
109
        $classMethod = 'on' . ucfirst($request->method);
110
        /** @var list<Link> $annotations */
111
        $annotations = (new ReflectionMethod($current::class, $classMethod))->getAnnotations();
112
        if ($link->type === LinkType::CRAWL_LINK) {
113
            return $this->annotationCrawl($annotations, $link, $current);
0 ignored issues
show
Bug introduced by
$annotations of type BEAR\Resource\list is incompatible with the type array expected by parameter $annotations of BEAR\Resource\Linker::annotationCrawl(). ( Ignorable by Annotation )

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

113
            return $this->annotationCrawl(/** @scrutinizer ignore-type */ $annotations, $link, $current);
Loading history...
114
        }
115
116
        /* @noinspection ExceptionsAnnotatingAndHandlingInspection */
117
        return $this->annotationRel($annotations, $link, $current);
0 ignored issues
show
Bug introduced by
$annotations of type BEAR\Resource\list is incompatible with the type array expected by parameter $annotations of BEAR\Resource\Linker::annotationRel(). ( Ignorable by Annotation )

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

117
        return $this->annotationRel(/** @scrutinizer ignore-type */ $annotations, $link, $current);
Loading history...
118
    }
119
120
    /**
121
     * Annotation link (new, self)
122
     *
123
     * @param Link[] $annotations
124
     *
125
     * @throws UriException
126
     * @throws MethodException
127
     * @throws Exception\LinkQueryException
128
     * @throws Exception\LinkRelException
129
     */
130
    private function annotationRel(array $annotations, LinkType $link, ResourceObject $current): ResourceObject
131
    {
132
        /* @noinspection LoopWhichDoesNotLoopInspection */
133
        foreach ($annotations as $annotation) {
134
            if ($annotation->rel !== $link->key) {
135
                continue;
136
            }
137
138
            $uri = uri_template($annotation->href, (array) $current->body);
139
            $rel = $this->factory->newInstance($uri);
140
            /* @noinspection UnnecessaryParenthesesInspection */
141
            $query = (new Uri($uri))->query;
142
            $request = new Request($this->invoker, $rel, Request::GET, $query);
143
144
            return $this->invoker->invoke($request);
145
        }
146
147
        throw new LinkRelException("rel:{$link->key} class:" . $current::class, 500);
148
    }
149
150
    /**
151
     * Link annotation crawl
152
     *
153
     * @param array<object> $annotations
154
     *
155
     * @throws MethodException
156
     */
157
    private function annotationCrawl(array $annotations, LinkType $link, ResourceObject $current): ResourceObject
158
    {
159
        $isList = $this->isList($current->body);
160
        /** @var array<array<string, mixed>> $bodyList */
161
        $bodyList = $isList ? (array) $current->body : [$current->body];
162
        foreach ($bodyList as &$body) {
163
            $this->crawl($annotations, $link, $body);
164
        }
165
166
        unset($body);
167
        $current->body = $isList ? $bodyList : $bodyList[0];
168
169
        return $current;
170
    }
171
172
    /**
173
     * @param array<object>        $annotations
174
     * @param array<string, mixed> $body
175
     *
176
     * @throws LinkQueryException
177
     * @throws MethodException
178
     * @throws LinkRelException
179
     * @throws UriException
180
     *
181
     * @param-out array $body
182
     */
183
    private function crawl(array $annotations, LinkType $link, array &$body): void
184
    {
185
        foreach ($annotations as $annotation) {
186
            if (! $annotation instanceof Link || $annotation->crawl !== $link->key) {
187
                continue;
188
            }
189
190
            $uri = uri_template($annotation->href, $body);
191
            $rel = $this->factory->newInstance($uri);
192
            /* @noinspection UnnecessaryParenthesesInspection */
193
            $query = (new Uri($uri))->query;
194
            $request = new Request($this->invoker, $rel, Request::GET, $query, [$link], $this);
195
            $hash = $request->hash();
196
            if (array_key_exists($hash, $this->cache)) {
197
                /** @var array<array<string, scalar|array<mixed>>>  $cachedResponse */
198
                $cachedResponse = $this->cache[$hash];
199
                $body[$annotation->rel] = $cachedResponse;
200
                continue;
201
            }
202
203
            $this->cache[$hash] = $body[$annotation->rel] = $this->getResponseBody($request);
204
        }
205
    }
206
207
    /** @return array<mixed> */
208
    private function getResponseBody(Request $request): array|null
209
    {
210
        $body = $this->invokeRecursive($request)->body;
211
        assert(is_array($body) || $body === null);
212
213
        return $body;
214
    }
215
216
    private function isList(mixed $value): bool
217
    {
218
        assert(is_array($value));
219
        /** @var array<array<mixed>|string> $list */
220
        $list = $value;
221
        /** @var array<mixed> $firstRow */
222
        $firstRow = array_pop($list);
223
        /** @var array<string, mixed>|string $firstRow */
224
        $keys = array_keys((array) $firstRow);
225
        $isMultiColumnMultiRowList = $this->isMultiColumnMultiRowList($keys, $list);
226
        $isMultiColumnList = $this->isMultiColumnList($value, $firstRow);
227
        $isSingleColumnList = $this->isSingleColumnList($value, $keys, $list);
228
229
        return $isSingleColumnList || $isMultiColumnMultiRowList || $isMultiColumnList;
230
    }
231
232
    /**
233
     * @param array<int, int|string>     $keys
234
     * @param array<array<mixed>|string> $list
235
     */
236
    private function isMultiColumnMultiRowList(array $keys, array $list): bool
237
    {
238
        if ($keys === [0 => 0]) {
239
            return false;
240
        }
241
242
        foreach ($list as $item) {
243
            if ($keys !== array_keys((array) $item)) {
244
                return false;
245
            }
246
        }
247
248
        return true;
249
    }
250
251
    /**
252
     * @param array<int|string, mixed> $value
253
     * @psalm-param array<string, mixed>|scalar $firstRow
254
     */
255
    private function isMultiColumnList(array $value, mixed $firstRow): bool
256
    {
257
        return is_array($firstRow) && array_filter(array_keys($value), 'is_numeric') === array_keys($value);
258
    }
259
260
    /**
261
     * @param array<int|string, mixed> $value
262
     * @param list<array-key>          $keys
0 ignored issues
show
Bug introduced by
The type BEAR\Resource\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
263
     * @param array<mixed, mixed>      $list
264
     */
265
    private function isSingleColumnList(array $value, array $keys, array $list): bool
266
    {
267
        return (count($value) === 1) && $keys === array_keys($list);
268
    }
269
}
270