Issues (141)

src/Linker.php (13 issues)

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 is_numeric;
23
use function ucfirst;
24
use function uri_template;
25
26
/**
27
 * @psalm-import-type Body from Types
28
 * @psalm-import-type BodyOrStringList from Types
29
 * @psalm-import-type ObjectList from Types
30
 * @psalm-import-type Query from Types
31
 * @psalm-import-type QueryList from Types
32
 */
33
final class Linker implements LinkerInterface
34
{
35
    /**
36
     * memory cache for linker
37
     *
38
     * @var Query
0 ignored issues
show
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...
39
     */
40
    private array $cache = [];
41
42
    public function __construct(
43
        private readonly InvokerInterface $invoker,
44
        private readonly FactoryInterface $factory,
45
    ) {
46
    }
47
48
    /**
49
     * {@inheritDoc}
50
     */
51
    #[Override]
52
    public function invoke(AbstractRequest $request)
53
    {
54
        $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...
55
56
        return $this->invokeRecursive($request);
57
    }
58
59
    /**
60
     * @throws LinkQueryException
61
     * @throws LinkRelException
62
     */
63
    private function invokeRecursive(AbstractRequest $request): ResourceObject
64
    {
65
        $this->invoker->invoke($request);
66
        $current = clone $request->resourceObject;
67
        if ($current->code >= Code::BAD_REQUEST) {
68
            return $current;
69
        }
70
71
        foreach ($request->links as $link) {
72
            /** @var Body $nextBody */
73
            $nextBody = $this->annotationLink($link, $current, $request)->body;
74
            $current = $this->nextLink($link, $current, $nextBody);
0 ignored issues
show
$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

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

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

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

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

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

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