Linker   A
last analyzed

Complexity

Total Complexity 37

Size/Duplication

Total Lines 246
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 11
Bugs 1 Features 1
Metric Value
eloc 76
dl 0
loc 246
rs 9.44
c 11
b 1
f 1
ccs 78
cts 78
cp 1
wmc 37

13 Methods

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

117
            return $this->annotationCrawl(/** @scrutinizer ignore-type */ $annotations, $link, $current);
Loading history...
118
        }
119
120
        /* @noinspection ExceptionsAnnotatingAndHandlingInspection */
121
        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

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