Linker::crawl()   A
last analyzed

Complexity

Conditions 5
Paths 4

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 13
nc 4
nop 3
dl 0
loc 21
rs 9.5222
c 0
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
 * @psalm-import-type Body from Types
27
 * @psalm-import-type BodyOrStringList from Types
28
 * @psalm-import-type ObjectList from Types
29
 * @psalm-import-type Query from Types
30
 * @psalm-import-type QueryList from Types
31
 */
32
final class Linker implements LinkerInterface
33
{
34
    /**
35
     * memory cache for linker
36
     *
37
     * @var Query
0 ignored issues
show
Bug introduced by
The type BEAR\Resource\Query 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...
38
     */
39
    private array $cache = [];
40
41
    public function __construct(
42
        private readonly InvokerInterface $invoker,
43
        private readonly FactoryInterface $factory,
44
    ) {
45
    }
46
47
    /**
48
     * {@inheritDoc}
49
     */
50
    #[Override]
51
    public function invoke(AbstractRequest $request)
52
    {
53
        $this->cache = [];
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type BEAR\Resource\Query of property $cache.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
54
55
        return $this->invokeRecursive($request);
56
    }
57
58
    /**
59
     * @throws LinkQueryException
60
     * @throws LinkRelException
61
     */
62
    private function invokeRecursive(AbstractRequest $request): ResourceObject
63
    {
64
        $this->invoker->invoke($request);
65
        $current = clone $request->resourceObject;
66
        if ($current->code >= Code::BAD_REQUEST) {
67
            return $current;
68
        }
69
70
        foreach ($request->links as $link) {
71
            /** @var Body $nextBody */
72
            $nextBody = $this->annotationLink($link, $current, $request)->body;
73
            $current = $this->nextLink($link, $current, $nextBody);
0 ignored issues
show
Bug introduced by
$nextBody of type BEAR\Resource\Body is incompatible with the type array expected by parameter $nextResource of BEAR\Resource\Linker::nextLink(). ( Ignorable by Annotation )

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

73
            $current = $this->nextLink($link, $current, /** @scrutinizer ignore-type */ $nextBody);
Loading history...
74
        }
75
76
        return $current;
77
    }
78
79
    /**
80
     * @param Body $nextResource
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...
81
     *
82
     * @return ResourceObject
83
     */
84
    private function nextLink(LinkType $link, ResourceObject $ro, array $nextResource): ResourceObject
85
    {
86
        /** @psalm-suppress MixedAssignment */
87
        $nextBody = $nextResource;
88
89
        if ($link->type === LinkType::SELF_LINK) {
90
            $ro->body = $nextBody;
91
92
            return $ro;
93
        }
94
95
        if ($link->type === LinkType::NEW_LINK) {
96
            assert(is_array($ro->body) || $ro->body === null);
97
            $ro->body[$link->key] = $nextBody;
98
99
            return $ro;
100
        }
101
102
        // crawl
103
        return $ro;
104
    }
105
106
    /**
107
     * Annotation link
108
     *
109
     * @throws MethodException
110
     * @throws LinkRelException
111
     * @throws Exception\LinkQueryException
112
     */
113
    private function annotationLink(LinkType $link, ResourceObject $current, AbstractRequest $request): ResourceObject
114
    {
115
        if (! is_array($current->body)) {
116
            throw new Exception\LinkQueryException('Only array is allowed for link in ' . $current::class, 500);
117
        }
118
119
        $classMethod = 'on' . ucfirst($request->method);
120
        /** @var list<Link> $annotations */
121
        $annotations = (new ReflectionMethod($current::class, $classMethod))->getAnnotations();
122
        if ($link->type === LinkType::CRAWL_LINK) {
123
            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

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

127
        return $this->annotationRel(/** @scrutinizer ignore-type */ $annotations, $link, $current);
Loading history...
128
    }
129
130
    /**
131
     * Annotation link (new, self)
132
     *
133
     * @param Link[] $annotations
134
     *
135
     * @throws UriException
136
     * @throws MethodException
137
     * @throws Exception\LinkQueryException
138
     * @throws Exception\LinkRelException
139
     */
140
    private function annotationRel(array $annotations, LinkType $link, ResourceObject $current): ResourceObject
141
    {
142
        /* @noinspection LoopWhichDoesNotLoopInspection */
143
        foreach ($annotations as $annotation) {
144
            if ($annotation->rel !== $link->key) {
145
                continue;
146
            }
147
148
            $uri = uri_template($annotation->href, (array) $current->body);
149
            $rel = $this->factory->newInstance($uri);
150
            /* @noinspection UnnecessaryParenthesesInspection */
151
            $query = (new Uri($uri))->query;
152
            $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

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

205
            $request = new Request($this->invoker, $rel, Request::GET, /** @scrutinizer ignore-type */ $query, [$link], $this);
Loading history...
206
            $hash = $request->hash();
207
            if (array_key_exists($hash, $this->cache)) {
208
                /** @var Body $cachedResponse */
209
                $cachedResponse = $this->cache[$hash];
210
                $body[$annotation->rel] = $cachedResponse;
211
                continue;
212
            }
213
214
            $this->cache[$hash] = $body[$annotation->rel] = $this->getResponseBody($request);
215
        }
216
    }
217
218
    /** @return Body|null */
219
    private function getResponseBody(Request $request): array|null
220
    {
221
        $body = $this->invokeRecursive($request)->body;
222
        assert(is_array($body) || $body === null);
223
224
        return $body;
225
    }
226
227
    private function isList(mixed $value): bool
228
    {
229
        assert(is_array($value));
230
        /** @var BodyOrStringList $list */
231
        $list = $value;
232
        /** @var Body $firstRow */
233
        $firstRow = array_pop($list);
0 ignored issues
show
Bug introduced by
$list of type BEAR\Resource\BodyOrStringList is incompatible with the type array expected by parameter $array of array_pop(). ( Ignorable by Annotation )

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

233
        $firstRow = array_pop(/** @scrutinizer ignore-type */ $list);
Loading history...
234
        /** @var Query|string $firstRow */
235
        $keys = array_keys((array) $firstRow);
236
        $isMultiColumnMultiRowList = $this->isMultiColumnMultiRowList($keys, $list);
237
        $isMultiColumnList = $this->isMultiColumnList($value, $firstRow);
238
        $isSingleColumnList = $this->isSingleColumnList($value, $keys, $list);
239
240
        return $isSingleColumnList || $isMultiColumnMultiRowList || $isMultiColumnList;
241
    }
242
243
    /**
244
     * @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...
245
     * @param BodyOrStringList $list
0 ignored issues
show
Bug introduced by
The type BEAR\Resource\BodyOrStringList 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...
246
     */
247
    private function isMultiColumnMultiRowList(array $keys, array $list): bool
248
    {
249
        if ($keys === [0 => 0]) {
250
            return false;
251
        }
252
253
        foreach ($list as $item) {
254
            if ($keys !== array_keys((array) $item)) {
255
                return false;
256
            }
257
        }
258
259
        return true;
260
    }
261
262
    /**
263
     * @param Body $value
264
     * @psalm-param Query|scalar $firstRow
265
     */
266
    private function isMultiColumnList(array $value, mixed $firstRow): bool
267
    {
268
        return is_array($firstRow) && array_filter(array_keys($value), 'is_numeric') === array_keys($value);
269
    }
270
271
    /**
272
     * @param Body            $value
273
     * @param list<array-key> $keys
274
     * @param Body            $list
275
     */
276
    private function isSingleColumnList(array $value, array $keys, array $list): bool
277
    {
278
        return (count($value) === 1) && $keys === array_keys($list);
279
    }
280
}
281