Linker::nextLink()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 3
nop 3
dl 0
loc 19
rs 9.9666
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\Di\Di\Set;
14
use Ray\Di\ProviderInterface;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, BEAR\Resource\ProviderInterface. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
15
use ReflectionMethod;
16
17
use function array_map;
18
use function assert;
19
use function is_array;
20
use function ucfirst;
21
use function uri_template;
22
23
/** @psalm-import-type Body from Types */
24
final class Linker implements LinkerInterface
25
{
26
    /** @param ProviderInterface<LinkCrawlerInterface> $linkCrawlerProvider */
27
    public function __construct(
28
        private readonly InvokerInterface $invoker,
29
        private readonly FactoryInterface $factory,
30
        #[Set(LinkCrawlerInterface::class)]
31
        private readonly ProviderInterface $linkCrawlerProvider,
32
    ) {
33
    }
34
35
    #[Override]
36
    public function invoke(AbstractRequest $request)
37
    {
38
        $linkCrawler = $this->linkCrawlerProvider->get();
39
40
        return $this->invokeRecursive($request, $linkCrawler);
41
    }
42
43
    /**
44
     * @throws LinkQueryException
45
     * @throws LinkRelException
46
     */
47
    private function invokeRecursive(AbstractRequest $request, LinkCrawlerInterface $linkCrawler): ResourceObject
48
    {
49
        $this->invoker->invoke($request);
50
        $current = clone $request->resourceObject;
51
        if ($current->code >= Code::BAD_REQUEST) {
52
            return $current;
53
        }
54
55
        foreach ($request->links as $link) {
56
            /** @var Body $nextBody */
57
            $nextBody = $this->annotationLink($link, $current, $request, $linkCrawler)->body;
58
            $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

58
            $current = $this->nextLink($link, $current, /** @scrutinizer ignore-type */ $nextBody);
Loading history...
59
        }
60
61
        return $current;
62
    }
63
64
    /** @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...
65
    private function nextLink(LinkType $link, ResourceObject $ro, array $nextResource): ResourceObject
66
    {
67
        $nextBody = $nextResource;
68
69
        if ($link->type === LinkType::SELF_LINK) {
70
            $ro->body = $nextBody;
71
72
            return $ro;
73
        }
74
75
        if ($link->type === LinkType::NEW_LINK) {
76
            assert(is_array($ro->body) || $ro->body === null);
77
            $ro->body[$link->key] = $nextBody;
78
79
            return $ro;
80
        }
81
82
        // crawl
83
        return $ro;
84
    }
85
86
    /**
87
     * Annotation link
88
     *
89
     * @throws MethodException
90
     * @throws LinkRelException
91
     * @throws Exception\LinkQueryException
92
     */
93
    private function annotationLink(LinkType $link, ResourceObject $current, AbstractRequest $request, LinkCrawlerInterface $linkCrawler): ResourceObject
94
    {
95
        if (! is_array($current->body)) {
96
            throw new Exception\LinkQueryException('Only array is allowed for link in ' . $current::class, 500);
97
        }
98
99
        $annotations = $this->getLinkAnnotations($current, $request->method);
100
        if ($link->type === LinkType::CRAWL_LINK) {
101
            return $this->annotationCrawl($annotations, $link, $current, $linkCrawler);
102
        }
103
104
        return $this->annotationRel($annotations, $link, $current);
105
    }
106
107
    /**
108
     * Get Link annotations from a ResourceObject method using PHP 8 attributes
109
     *
110
     * @return list<Link>
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...
111
     */
112
    private function getLinkAnnotations(ResourceObject $ro, string $method): array
113
    {
114
        $classMethod = 'on' . ucfirst($method);
115
        $refMethod = new ReflectionMethod($ro, $classMethod);
116
        $attributes = $refMethod->getAttributes(Link::class);
117
118
        return array_map(
0 ignored issues
show
Bug Best Practice introduced by
The expression return array_map(functio... ... */ }, $attributes) returns the type array which is incompatible with the documented return type BEAR\Resource\list.
Loading history...
119
            static fn ($attr) => $attr->newInstance(),
120
            $attributes,
121
        );
122
    }
123
124
    /**
125
     * Annotation link (new, self)
126
     *
127
     * @param list<Link> $annotations
128
     *
129
     * @throws UriException
130
     * @throws MethodException
131
     * @throws Exception\LinkQueryException
132
     * @throws Exception\LinkRelException
133
     */
134
    private function annotationRel(array $annotations, LinkType $link, ResourceObject $current): ResourceObject
135
    {
136
        foreach ($annotations as $annotation) {
137
            if ($annotation->rel !== $link->key) {
138
                continue;
139
            }
140
141
            $uri = uri_template($annotation->href, (array) $current->body);
142
            $rel = $this->factory->newInstance($uri);
143
            $query = (new Uri($uri))->query;
144
            $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

144
            $request = new Request($this->invoker, $rel, Request::GET, /** @scrutinizer ignore-type */ $query);
Loading history...
145
146
            return $this->invoker->invoke($request);
147
        }
148
149
        throw new LinkRelException("rel:{$link->key} class:" . $current::class, 500);
150
    }
151
152
    /**
153
     * Link annotation crawl - delegate to LinkCrawlerInterface
154
     *
155
     * @param list<Link> $annotations
156
     */
157
    private function annotationCrawl(array $annotations, LinkType $link, ResourceObject $current, LinkCrawlerInterface $linkCrawler): ResourceObject
158
    {
159
        $isList = $linkCrawler->isList($current->body);
160
        /** @var list<array<string, mixed>> $bodyList */
161
        $bodyList = $isList ? (array) $current->body : [$current->body];
162
163
        // Delegate to LinkCrawler
164
        $linkCrawler->crawl($annotations, $link, $bodyList);
0 ignored issues
show
Bug introduced by
$bodyList of type BEAR\Resource\list is incompatible with the type array expected by parameter $bodyList of BEAR\Resource\LinkCrawlerInterface::crawl(). ( Ignorable by Annotation )

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

164
        $linkCrawler->crawl($annotations, $link, /** @scrutinizer ignore-type */ $bodyList);
Loading history...
165
166
        $current->body = $isList ? $bodyList : $bodyList[0];
167
168
        return $current;
169
    }
170
}
171