Linker::annotationCrawl()   A
last analyzed

Complexity

Conditions 4
Paths 8

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 7
nc 8
nop 3
dl 0
loc 13
ccs 5
cts 5
cp 1
crap 4
rs 10
c 1
b 0
f 0
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
/**
26
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
27
 * @psalm-import-type Body from Types
28
 */
29
final class Linker implements LinkerInterface
30
{
31
    /**
32
     * memory cache for linker
33
     *
34
     * @var array<string, mixed>
35
     */
36
    private array $cache = [];
37
38
    public function __construct(
39
        private readonly InvokerInterface $invoker,
40 54
        private readonly FactoryInterface $factory,
41
    ) {
42
    }
43
44
    /**
45 54
     * {@inheritDoc}
46 54
     */
47 54
    #[Override]
48 54
    public function invoke(AbstractRequest $request)
49
    {
50
        $this->cache = [];
51
52
        return $this->invokeRecursive($request);
53
    }
54
55
    /**
56 10
     * @throws LinkQueryException
57
     * @throws LinkRelException
58 10
     */
59 10
    private function invokeRecursive(AbstractRequest $request): ResourceObject
60 10
    {
61
        $this->invoker->invoke($request);
62 10
        $current = clone $request->resourceObject;
63 8
        if ($current->code >= Code::BAD_REQUEST) {
64
            return $current;
65
        }
66 8
67
        foreach ($request->links as $link) {
68
            /** @var array<mixed> $nextBody */
69
            $nextBody = $this->annotationLink($link, $current, $request)->body;
70
            $current = $this->nextLink($link, $current, $nextBody);
71
        }
72 8
73
        return $current;
74 8
    }
75
76 8
    /**
77 2
     * @param array<mixed> $nextResource
78
     *
79 2
     * @return ResourceObject
80
     */
81
    private function nextLink(LinkType $link, ResourceObject $ro, array $nextResource): ResourceObject
82 6
    {
83 2
        /** @psalm-suppress MixedAssignment */
84
        $nextBody = $nextResource;
85 2
86
        if ($link->type === LinkType::SELF_LINK) {
87
            $ro->body = $nextBody;
88
89 4
            return $ro;
90
        }
91
92
        if ($link->type === LinkType::NEW_LINK) {
93
            assert(is_array($ro->body) || $ro->body === null);
94
            $ro->body[$link->key] = $nextBody;
95
96
            return $ro;
97
        }
98
99
        // crawl
100
        return $ro;
101 10
    }
102
103 10
    /**
104 1
     * Annotation link
105
     *
106 9
     * @throws MethodException
107 9
     * @throws LinkRelException
108 9
     * @throws Exception\LinkQueryException
109 4
     */
110
    private function annotationLink(LinkType $link, ResourceObject $current, AbstractRequest $request): ResourceObject
111
    {
112
        if (! is_array($current->body)) {
113 5
            throw new Exception\LinkQueryException('Only array is allowed for link in ' . $current::class, 500);
114
        }
115
116
        $classMethod = 'on' . ucfirst($request->method);
117
        /** @var list<Link> $annotations */
118
        $annotations = (new ReflectionMethod($current::class, $classMethod))->getAnnotations();
119
        if ($link->type === LinkType::CRAWL_LINK) {
120
            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

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

124
        return $this->annotationRel(/** @scrutinizer ignore-type */ $annotations, $link, $current);
Loading history...
125
    }
126 5
127
    /**
128
     * Annotation link (new, self)
129 5
     *
130 5
     * @param Link[] $annotations
131 1
     *
132
     * @throws UriException
133 4
     * @throws MethodException
134 4
     * @throws Exception\LinkQueryException
135
     * @throws Exception\LinkRelException
136 4
     */
137
    private function annotationRel(array $annotations, LinkType $link, ResourceObject $current): ResourceObject
138 4
    {
139
        /* @noinspection LoopWhichDoesNotLoopInspection */
140
        foreach ($annotations as $annotation) {
141 1
            if ($annotation->rel !== $link->key) {
142
                continue;
143
            }
144
145
            $uri = uri_template($annotation->href, (array) $current->body);
146
            $rel = $this->factory->newInstance($uri);
147
            /* @noinspection UnnecessaryParenthesesInspection */
148
            $query = (new Uri($uri))->query;
149 4
            $request = new Request($this->invoker, $rel, Request::GET, $query);
0 ignored issues
show
Bug introduced by
$query of type BEAR\Resource\Query is incompatible with the type array expected by parameter $query of BEAR\Resource\Request::__construct(). ( Ignorable by Annotation )

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

149
            $request = new Request($this->invoker, $rel, Request::GET, /** @scrutinizer ignore-type */ $query);
Loading history...
150
151 4
            return $this->invoker->invoke($request);
152 4
        }
153 4
154
        throw new LinkRelException("rel:{$link->key} class:" . $current::class, 500);
155 4
    }
156
157 4
    /**
158 4
     * Link annotation crawl
159
     *
160 4
     * @param array<object> $annotations
161
     *
162
     * @throws MethodException
163
     */
164
    private function annotationCrawl(array $annotations, LinkType $link, ResourceObject $current): ResourceObject
165
    {
166
        $isList = $this->isList($current->body);
167
        /** @var array<array<string, mixed>> $bodyList */
168
        $bodyList = $isList ? (array) $current->body : [$current->body];
169 4
        foreach ($bodyList as &$body) {
170
            $this->crawl($annotations, $link, $body);
171 4
        }
172
173 4
        unset($body);
174 3
        $current->body = $isList ? $bodyList : $bodyList[0];
175
176 4
        return $current;
177 4
    }
178
179 4
    /**
180 4
     * @param array<object> $annotations
181 4
     * @param Body          $body
0 ignored issues
show
Bug introduced by
The type BEAR\Resource\Body 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...
182 3
     *
183
     * @throws LinkQueryException
184 3
     * @throws MethodException
185
     * @throws LinkRelException
186 4
     * @throws UriException
187
     *
188 4
     * @param-out array $body
189
     */
190 4
    private function crawl(array $annotations, LinkType $link, array &$body): void
191
    {
192 4
        foreach ($annotations as $annotation) {
193 4
            if (! $annotation instanceof Link || $annotation->crawl !== $link->key) {
194 4
                continue;
195 4
            }
196 4
197 4
            $uri = uri_template($annotation->href, $body);
198
            $rel = $this->factory->newInstance($uri);
199 4
            /* @noinspection UnnecessaryParenthesesInspection */
200
            $query = (new Uri($uri))->query;
201
            $request = new Request($this->invoker, $rel, Request::GET, $query, [$link], $this);
0 ignored issues
show
Bug introduced by
$query of type BEAR\Resource\Query is incompatible with the type array expected by parameter $query of BEAR\Resource\Request::__construct(). ( Ignorable by Annotation )

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

201
            $request = new Request($this->invoker, $rel, Request::GET, /** @scrutinizer ignore-type */ $query, [$link], $this);
Loading history...
202 4
            $hash = $request->hash();
203
            if (array_key_exists($hash, $this->cache)) {
204 4
                /** @var array<array<string, scalar|array<mixed>>>  $cachedResponse */
205 4
                $cachedResponse = $this->cache[$hash];
206
                $body[$annotation->rel] = $cachedResponse;
207
                continue;
208 4
            }
209 4
210 4
            $this->cache[$hash] = $body[$annotation->rel] = $this->getResponseBody($request);
211
        }
212
    }
213
214 4
    /** @return array<mixed> */
215
    private function getResponseBody(Request $request): array|null
216
    {
217 4
        $body = $this->invokeRecursive($request)->body;
218
        assert(is_array($body) || $body === null);
219 4
220
        return $body;
221
    }
222 4
223
    private function isList(mixed $value): bool
224 4
    {
225
        assert(is_array($value));
226
        /** @var array<array<mixed>|string> $list */
227
        $list = $value;
228
        /** @var array<mixed> $firstRow */
229
        $firstRow = array_pop($list);
230
        /** @var array<string, mixed>|string $firstRow */
231
        $keys = array_keys((array) $firstRow);
232
        $isMultiColumnMultiRowList = $this->isMultiColumnMultiRowList($keys, $list);
233
        $isMultiColumnList = $this->isMultiColumnList($value, $firstRow);
234
        $isSingleColumnList = $this->isSingleColumnList($value, $keys, $list);
235
236
        return $isSingleColumnList || $isMultiColumnMultiRowList || $isMultiColumnList;
237
    }
238
239
    /**
240
     * @param array<int, int|string>     $keys
241
     * @param array<array<mixed>|string> $list
242
     */
243
    private function isMultiColumnMultiRowList(array $keys, array $list): bool
244
    {
245
        if ($keys === [0 => 0]) {
246
            return false;
247
        }
248
249
        foreach ($list as $item) {
250
            if ($keys !== array_keys((array) $item)) {
251
                return false;
252
            }
253
        }
254
255
        return true;
256
    }
257
258
    /**
259
     * @param array<int|string, mixed> $value
260
     * @psalm-param array<string, mixed>|scalar $firstRow
261
     */
262
    private function isMultiColumnList(array $value, mixed $firstRow): bool
263
    {
264
        return is_array($firstRow) && array_filter(array_keys($value), 'is_numeric') === array_keys($value);
265
    }
266
267
    /**
268
     * @param array<int|string, mixed> $value
269
     * @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...
270
     * @param array<mixed, mixed>      $list
271
     */
272
    private function isSingleColumnList(array $value, array $keys, array $list): bool
273
    {
274
        return (count($value) === 1) && $keys === array_keys($list);
275
    }
276
}
277