Issues (169)

src/Linker.php (1 issue)

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;
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);
59
        }
60
61
        return $current;
62
    }
63
64
    /** @param Body $nextResource */
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>
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);
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);
165
166
        $current->body = $isList ? $bodyList : $bodyList[0];
167
168
        return $current;
169
    }
170
}
171