Passed
Push — claude/understand-crawl-pLOr9 ( efe806 )
by Akihito
02:23
created

Linker::processBatchLinks()   B

Complexity

Conditions 9
Paths 16

Size

Total Lines 37
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 19
c 0
b 0
f 0
nc 16
nop 3
dl 0
loc 37
rs 8.0555
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BEAR\Resource;
6
7
use BEAR\Resource\Annotation\Link;
8
use BEAR\Resource\Batch\BatchResolverFactoryInterface;
9
use BEAR\Resource\Batch\BatchResolverInterface;
10
use BEAR\Resource\Batch\Requests;
11
use BEAR\Resource\Exception\LinkQueryException;
12
use BEAR\Resource\Exception\LinkRelException;
13
use BEAR\Resource\Exception\MethodException;
14
use BEAR\Resource\Exception\UriException;
15
use Override;
16
use Ray\Aop\ReflectionMethod;
17
18
use function array_filter;
19
use function array_key_exists;
20
use function array_keys;
21
use function array_pop;
22
use function array_values;
23
use function assert;
24
use function count;
25
use function is_array;
26
use function is_numeric;
27
use function ucfirst;
28
use function uri_template;
29
30
/**
31
 * @psalm-import-type Body from Types
32
 * @psalm-import-type BodyOrStringList from Types
33
 * @psalm-import-type ObjectList from Types
34
 * @psalm-import-type Query from Types
35
 * @psalm-import-type QueryList from Types
36
 */
37
final class Linker implements LinkerInterface
38
{
39
    /**
40
     * memory cache for linker
41
     *
42
     * @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...
43
     */
44
    private array $cache = [];
45
46
    /** @var array<class-string<BatchResolverInterface>, BatchResolverInterface> */
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string<Batch...BatchResolverInterface> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string<BatchResolverInterface>, BatchResolverInterface>.
Loading history...
47
    private array $batchResolverCache = [];
48
49
    public function __construct(
50
        private readonly InvokerInterface $invoker,
51
        private readonly FactoryInterface $factory,
52
        private readonly BatchResolverFactoryInterface|null $batchResolverFactory = null,
53
    ) {
54
    }
55
56
    /**
57
     * {@inheritDoc}
58
     */
59
    #[Override]
60
    public function invoke(AbstractRequest $request)
61
    {
62
        $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...
63
64
        return $this->invokeRecursive($request);
65
    }
66
67
    /**
68
     * @throws LinkQueryException
69
     * @throws LinkRelException
70
     */
71
    private function invokeRecursive(AbstractRequest $request): ResourceObject
72
    {
73
        $this->invoker->invoke($request);
74
        $current = clone $request->resourceObject;
75
        if ($current->code >= Code::BAD_REQUEST) {
76
            return $current;
77
        }
78
79
        foreach ($request->links as $link) {
80
            /** @var Body $nextBody */
81
            $nextBody = $this->annotationLink($link, $current, $request)->body;
82
            $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

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

132
            return $this->annotationCrawl(/** @scrutinizer ignore-type */ $annotations, $link, $current);
Loading history...
133
        }
134
135
        /* @noinspection ExceptionsAnnotatingAndHandlingInspection */
136
        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

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

161
            $request = new Request($this->invoker, $rel, Request::GET, /** @scrutinizer ignore-type */ $query);
Loading history...
162
163
            return $this->invoker->invoke($request);
164
        }
165
166
        throw new LinkRelException("rel:{$link->key} class:" . $current::class, 500);
167
    }
168
169
    /**
170
     * Link annotation crawl
171
     *
172
     * @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...
173
     *
174
     * @throws MethodException
175
     */
176
    private function annotationCrawl(array $annotations, LinkType $link, ResourceObject $current): ResourceObject
177
    {
178
        $isList = $this->isList($current->body);
179
        /** @var QueryList $bodyList */
180
        $bodyList = $isList ? (array) $current->body : [$current->body];
181
182
        // Process batch links first
183
        $this->processBatchLinks($annotations, $link, $bodyList);
0 ignored issues
show
Bug introduced by
$bodyList of type BEAR\Resource\QueryList is incompatible with the type array expected by parameter $bodyList of BEAR\Resource\Linker::processBatchLinks(). ( Ignorable by Annotation )

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

183
        $this->processBatchLinks($annotations, $link, /** @scrutinizer ignore-type */ $bodyList);
Loading history...
184
185
        // Process non-batch links
186
        foreach ($bodyList as &$body) {
187
            $this->crawl($annotations, $link, $body);
188
        }
189
190
        unset($body);
191
        /** @psalm-suppress PossiblyUndefinedArrayOffset */
192
        $current->body = $isList ? $bodyList : $bodyList[0];
193
194
        return $current;
195
    }
196
197
    /**
198
     * Process batch-enabled links
199
     *
200
     * @param ObjectList $annotations
201
     * @param QueryList  $bodyList
0 ignored issues
show
Bug introduced by
The type BEAR\Resource\QueryList 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...
202
     *
203
     * @param-out QueryList $bodyList
204
     */
205
    private function processBatchLinks(array $annotations, LinkType $link, array &$bodyList): void
206
    {
207
        if ($this->batchResolverFactory === null) {
208
            return;
209
        }
210
211
        // Group URIs by batch resolver class
212
        /** @var array<class-string<BatchResolverInterface>, array{annotation: Link, uris: array<int, string>}> $batchGroups */
213
        $batchGroups = [];
214
215
        foreach ($annotations as $annotation) {
216
            if (! $annotation instanceof Link || $annotation->crawl !== $link->key) {
217
                continue;
218
            }
219
220
            if ($annotation->batch === null) {
221
                continue;
222
            }
223
224
            /** @var class-string<BatchResolverInterface> $batchClass */
225
            $batchClass = $annotation->batch;
226
            $batchGroups[$batchClass] = ['annotation' => $annotation, 'uris' => []];
227
228
            foreach ($bodyList as $index => $body) {
229
                $uri = uri_template($annotation->href, $body);
230
                $batchGroups[$batchClass]['uris'][$index] = $uri;
231
            }
232
        }
233
234
        // Execute batch resolvers and distribute results
235
        foreach ($batchGroups as $batchClass => $group) {
236
            $resolver = $this->getBatchResolver($batchClass);
237
            $requests = new Requests(array_values($group['uris']));
238
            $results = $resolver($requests);
239
240
            foreach ($group['uris'] as $index => $uri) {
241
                $bodyList[$index][$group['annotation']->rel] = $results->get($uri);
242
            }
243
        }
244
    }
245
246
    /**
247
     * Get or create a batch resolver instance
248
     *
249
     * @param class-string<BatchResolverInterface> $class
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<BatchResolverInterface> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<BatchResolverInterface>.
Loading history...
250
     */
251
    private function getBatchResolver(string $class): BatchResolverInterface
252
    {
253
        if (! isset($this->batchResolverCache[$class])) {
254
            assert($this->batchResolverFactory !== null);
255
            $this->batchResolverCache[$class] = $this->batchResolverFactory->create($class);
256
        }
257
258
        return $this->batchResolverCache[$class];
259
    }
260
261
    /**
262
     * @param ObjectList $annotations
263
     * @param Body       $body
264
     *
265
     * @throws LinkQueryException
266
     * @throws MethodException
267
     * @throws LinkRelException
268
     * @throws UriException
269
     *
270
     * @param-out Body $body
271
     */
272
    private function crawl(array $annotations, LinkType $link, array &$body): void
273
    {
274
        foreach ($annotations as $annotation) {
275
            if (! $annotation instanceof Link || $annotation->crawl !== $link->key) {
276
                continue;
277
            }
278
279
            // Skip batch-enabled links (already processed by processBatchLinks)
280
            if ($annotation->batch !== null && $this->batchResolverFactory !== null) {
281
                continue;
282
            }
283
284
            $uri = uri_template($annotation->href, $body);
285
            $rel = $this->factory->newInstance($uri);
286
            /* @noinspection UnnecessaryParenthesesInspection */
287
            $query = (new Uri($uri))->query;
288
            $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

288
            $request = new Request($this->invoker, $rel, Request::GET, /** @scrutinizer ignore-type */ $query, [$link], $this);
Loading history...
289
            $hash = $request->hash();
290
            if (array_key_exists($hash, $this->cache)) {
291
                /** @var Body $cachedResponse */
292
                $cachedResponse = $this->cache[$hash];
293
                $body[$annotation->rel] = $cachedResponse;
294
                continue;
295
            }
296
297
            $this->cache[$hash] = $body[$annotation->rel] = $this->getResponseBody($request);
298
        }
299
    }
300
301
    /** @return Body|null */
302
    private function getResponseBody(Request $request): array|null
303
    {
304
        $body = $this->invokeRecursive($request)->body;
305
        assert(is_array($body) || $body === null);
306
307
        return $body;
308
    }
309
310
    private function isList(mixed $value): bool
311
    {
312
        assert(is_array($value));
313
        /** @var BodyOrStringList $list */
314
        $list = $value;
315
        /** @var Body $firstRow */
316
        $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

316
        $firstRow = array_pop(/** @scrutinizer ignore-type */ $list);
Loading history...
317
        /** @var Query|string $firstRow */
318
        $keys = array_keys((array) $firstRow);
319
        $isMultiColumnMultiRowList = $this->isMultiColumnMultiRowList($keys, $list);
320
        $isMultiColumnList = $this->isMultiColumnList($value, $firstRow);
321
        $isSingleColumnList = $this->isSingleColumnList($value, $keys, $list);
322
323
        return $isSingleColumnList || $isMultiColumnMultiRowList || $isMultiColumnList;
324
    }
325
326
    /**
327
     * @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...
328
     * @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...
329
     */
330
    private function isMultiColumnMultiRowList(array $keys, array $list): bool
331
    {
332
        if ($keys === [0 => 0]) {
333
            return false;
334
        }
335
336
        foreach ($list as $item) {
337
            if ($keys !== array_keys((array) $item)) {
338
                return false;
339
            }
340
        }
341
342
        return true;
343
    }
344
345
    /**
346
     * @param Body $value
347
     * @psalm-param Query|scalar $firstRow
348
     */
349
    private function isMultiColumnList(array $value, mixed $firstRow): bool
350
    {
351
        return is_array($firstRow) && array_filter(array_keys($value), is_numeric(...)) === array_keys($value);
0 ignored issues
show
Bug introduced by
The type is_numeric 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...
352
    }
353
354
    /**
355
     * @param Body            $value
356
     * @param list<array-key> $keys
357
     * @param Body            $list
358
     */
359
    private function isSingleColumnList(array $value, array $keys, array $list): bool
360
    {
361
        return (count($value) === 1) && $keys === array_keys($list);
362
    }
363
}
364