Linker::getResponseBody()   A
last analyzed

Complexity

Conditions 2
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

155
            $request = new Request($this->invoker, $rel, Request::GET, /** @scrutinizer ignore-type */ $query);
Loading history...
156
157
            return $this->invoker->invoke($request);
158
        }
159
160
        throw new LinkRelException("rel:{$link->key} class:" . $current::class, 500);
161
    }
162
163
    /**
164
     * Link annotation crawl
165
     *
166
     * @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...
167
     *
168
     * @throws MethodException
169
     */
170
    private function annotationCrawl(array $annotations, LinkType $link, ResourceObject $current): ResourceObject
171
    {
172
        $isList = $this->isList($current->body);
173
        /** @var QueryList $bodyList */
174
        $bodyList = $isList ? (array) $current->body : [$current->body];
175
176
        // Process DataLoader-enabled links first
177
        $this->dataLoader?->load($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\DataLoader\DataLoader::load(). ( Ignorable by Annotation )

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

177
        $this->dataLoader?->load($annotations, $link, /** @scrutinizer ignore-type */ $bodyList);
Loading history...
178
179
        // Process non-DataLoader links
180
        /**
181
         * @psalm-suppress MixedAssignment
182
         * @psalm-suppress MixedArgument
183
         */
184
        foreach ($bodyList as &$body) {
185
            $this->crawl($annotations, $link, $body);
186
        }
187
188
        unset($body);
189
        /** @psalm-suppress PossiblyUndefinedArrayOffset, InvalidArrayAccess */
190
        $current->body = $isList ? $bodyList : $bodyList[0];
191
192
        return $current;
193
    }
194
195
    /**
196
     * @param ObjectList $annotations
197
     * @param Body       $body
198
     *
199
     * @throws LinkQueryException
200
     * @throws MethodException
201
     * @throws LinkRelException
202
     * @throws UriException
203
     *
204
     * @param-out Body $body
205
     */
206
    private function crawl(array $annotations, LinkType $link, array &$body): void
207
    {
208
        foreach ($annotations as $annotation) {
209
            if (! $annotation instanceof Link || $annotation->crawl !== $link->key) {
210
                continue;
211
            }
212
213
            // Skip DataLoader-enabled links (already processed by DataLoader)
214
            if ($annotation->dataLoader !== null && $this->dataLoader !== null) {
215
                continue;
216
            }
217
218
            $uri = uri_template($annotation->href, $body);
219
            $rel = $this->factory->newInstance($uri);
220
            /* @noinspection UnnecessaryParenthesesInspection */
221
            $query = (new Uri($uri))->query;
222
            $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

222
            $request = new Request($this->invoker, $rel, Request::GET, /** @scrutinizer ignore-type */ $query, [$link], $this);
Loading history...
223
            $hash = $request->hash();
224
            if (array_key_exists($hash, $this->cache)) {
225
                /** @var Body $cachedResponse */
226
                $cachedResponse = $this->cache[$hash];
227
                $body[$annotation->rel] = $cachedResponse;
228
                continue;
229
            }
230
231
            $this->cache[$hash] = $body[$annotation->rel] = $this->getResponseBody($request);
232
        }
233
    }
234
235
    /** @return Body|null */
236
    private function getResponseBody(Request $request): array|null
237
    {
238
        $body = $this->invokeRecursive($request)->body;
239
        assert(is_array($body) || $body === null);
240
241
        return $body;
242
    }
243
244
    private function isList(mixed $value): bool
245
    {
246
        assert(is_array($value));
247
        /** @var BodyOrStringList $list */
248
        $list = $value;
249
        /** @var Body $firstRow */
250
        $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

250
        $firstRow = array_pop(/** @scrutinizer ignore-type */ $list);
Loading history...
251
        /** @var Query|string $firstRow */
252
        $keys = array_keys((array) $firstRow);
253
        $isMultiColumnMultiRowList = $this->isMultiColumnMultiRowList($keys, $list);
254
        $isMultiColumnList = $this->isMultiColumnList($value, $firstRow);
255
        $isSingleColumnList = $this->isSingleColumnList($value, $keys, $list);
256
257
        return $isSingleColumnList || $isMultiColumnMultiRowList || $isMultiColumnList;
258
    }
259
260
    /**
261
     * @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...
262
     * @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...
263
     */
264
    private function isMultiColumnMultiRowList(array $keys, array $list): bool
265
    {
266
        if ($keys === [0 => 0]) {
267
            return false;
268
        }
269
270
        foreach ($list as $item) {
271
            if ($keys !== array_keys((array) $item)) {
272
                return false;
273
            }
274
        }
275
276
        return true;
277
    }
278
279
    /**
280
     * @param Body $value
281
     * @psalm-param Query|scalar $firstRow
282
     */
283
    private function isMultiColumnList(array $value, mixed $firstRow): bool
284
    {
285
        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...
286
    }
287
288
    /**
289
     * @param Body            $value
290
     * @param list<array-key> $keys
291
     * @param Body            $list
292
     */
293
    private function isSingleColumnList(array $value, array $keys, array $list): bool
294
    {
295
        return (count($value) === 1) && $keys === array_keys($list);
296
    }
297
}
298