Completed
Pull Request — 1.x (#223)
by Akihito
03:54
created

Linker   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 260
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 38
lcom 1
cbo 11
dl 0
loc 260
rs 9.36
c 0
b 0
f 0
ccs 78
cts 78
cp 1

12 Methods

Rating   Name   Duplication   Size   Complexity  
A annotationLink() 0 15 3
A annotationRel() 0 18 3
A __construct() 0 9 1
A invoke() 0 16 3
A nextLink() 0 21 5
A annotationCrawl() 0 14 4
A crawl() 0 22 5
A getResponseBody() 0 7 2
A isList() 0 16 4
A isMultiColumnMultiRowList() 0 13 4
A isMultiColumnList() 0 4 2
A isSingleColumnList() 0 4 2
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 Doctrine\Common\Annotations\Reader;
11
use function get_class;
12
use ReflectionMethod;
13
14
/**
15
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
16
 */
17
final class Linker implements LinkerInterface
18
{
19
    /**
20
     * @var Reader
21
     */
22
    private $reader;
23
24
    /**
25
     * @var InvokerInterface
26
     */
27
    private $invoker;
28
29
    /**
30
     * @var FactoryInterface
31
     */
32
    private $factory;
33
34
    /**
35
     * memory cache for linker
36
     *
37
     * @var array<string, mixed>
38
     */
39
    private $cache = [];
40 54
41
    public function __construct(
42
        Reader $reader,
43
        InvokerInterface $invoker,
44
        FactoryInterface $factory
45 54
    ) {
46 54
        $this->reader = $reader;
47 54
        $this->invoker = $invoker;
48 54
        $this->factory = $factory;
49
    }
50
51
    /**
52
     * {@inheritdoc}
53
     *
54
     * @throws LinkQueryException
55
     * @throws \BEAR\Resource\Exception\LinkRelException
56 10
     */
57
    public function invoke(AbstractRequest $request)
58 10
    {
59 10
        $this->invoker->invoke($request);
60 10
        $current = clone $request->resourceObject;
61
        if ($current->code >= Code::BAD_REQUEST) {
62 10
            return $current;
63 8
        }
64
65
        foreach ($request->links as $link) {
66 8
            /** @var array<mixed> $nextBody */
67
            $nextBody = $this->annotationLink($link, $current, $request)->body;
68
            $current = $this->nextLink($link, $current, $nextBody);
69
        }
70
71
        return $current;
72 8
    }
73
74 8
    /**
75
     * How next linked resource treated (add ? replace ?)
76 8
     *
77 2
     * @param mixed|ResourceObject $nextResource
78
     */
79 2
    private function nextLink(LinkType $link, ResourceObject $ro, $nextResource) : ResourceObject
80
    {
81
        /** @var array<mixed> $nextBody */
82 6
        $nextBody = $nextResource instanceof ResourceObject ? $nextResource->body : $nextResource;
83 2
84
        if ($link->type === LinkType::SELF_LINK) {
85 2
            $ro->body = $nextBody;
86
87
            return $ro;
88
        }
89 4
90
        if ($link->type === LinkType::NEW_LINK) {
91
            assert(is_array($ro->body) || $ro->body === null);
92
            $ro->body[$link->key] = $nextBody;
93
94
            return $ro;
95
        }
96
97
        // crawl
98
        return $ro;
99
    }
100
101 10
    /**
102
     * Annotation link
103 10
     *
104 1
     * @throws \BEAR\Resource\Exception\MethodException
105
     * @throws \BEAR\Resource\Exception\LinkRelException
106 9
     * @throws Exception\LinkQueryException
107 9
     */
108 9
    private function annotationLink(LinkType $link, ResourceObject $current, AbstractRequest $request) : ResourceObject
109 4
    {
110
        if (! is_array($current->body)) {
111
            throw new Exception\LinkQueryException('Only array is allowed for link in ' . get_class($current), 500);
112
        }
113 5
        $classMethod = 'on' . ucfirst($request->method);
114
        /** @var list<Link> $annotations */
0 ignored issues
show
Documentation introduced by
The doc-type list<Link> could not be parsed: Expected "|" or "end of type", but got "<" at position 4. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
115
        $annotations = $this->reader->getMethodAnnotations(new ReflectionMethod(get_class($current), $classMethod));
116
        if ($link->type === LinkType::CRAWL_LINK) {
117
            return $this->annotationCrawl($annotations, $link, $current);
118
        }
119
120
        /* @noinspection ExceptionsAnnotatingAndHandlingInspection */
121
        return $this->annotationRel($annotations, $link, $current);
122
    }
123
124
    /**
125
     * Annotation link (new, self)
126 5
     *
127
     * @param \BEAR\Resource\Annotation\Link[] $annotations
128
     *
129 5
     * @throws \BEAR\Resource\Exception\UriException
130 5
     * @throws \BEAR\Resource\Exception\MethodException
131 1
     * @throws Exception\LinkQueryException
132
     * @throws Exception\LinkRelException
133 4
     */
134 4
    private function annotationRel(array $annotations, LinkType $link, ResourceObject $current) : ResourceObject
135
    {
136 4
        /* @noinspection LoopWhichDoesNotLoopInspection */
137
        foreach ($annotations as $annotation) {
138 4
            if ($annotation->rel !== $link->key) {
139
                continue;
140
            }
141 1
            $uri = uri_template($annotation->href, (array) $current->body);
142
            $rel = $this->factory->newInstance($uri);
143
            /* @noinspection UnnecessaryParenthesesInspection */
144
            $query = (new Uri($uri))->query;
145
            $request = new Request($this->invoker, $rel, Request::GET, $query);
146
147
            return $this->invoker->invoke($request);
148
        }
149 4
150
        throw new LinkRelException("rel:{$link->key} class:" . get_class($current), 500);
151 4
    }
152 4
153 4
    /**
154
     * Link annotation crawl
155 4
     *
156
     * @param array<object> $annotations
157 4
     *
158 4
     * @throws \BEAR\Resource\Exception\MethodException
159
     */
160 4
    private function annotationCrawl(array $annotations, LinkType $link, ResourceObject $current) : ResourceObject
161
    {
162
        $isList = $this->isList($current->body);
163
        /** @var array<array<string, mixed>> $bodyList */
164
        $bodyList = $isList ? (array) $current->body : [$current->body];
165
        $this->cache = [];
166
        foreach ($bodyList as &$body) {
167
            $this->crawl($annotations, $link, $body);
168
        }
169 4
        unset($body);
170
        $current->body = $isList ? $bodyList : $bodyList[0];
171 4
172
        return $current;
173 4
    }
174 3
175
    /**
176 4
     * @param array<object>        $annotations
177 4
     * @param array<string, mixed> $body
178
     *
179 4
     * @param-out array $body
180 4
     *
181 4
     * @throws \BEAR\Resource\Exception\LinkQueryException
182 3
     * @throws \BEAR\Resource\Exception\MethodException
183
     * @throws \BEAR\Resource\Exception\LinkRelException
184 3
     * @throws \BEAR\Resource\Exception\UriException
185
     */
186 4
    private function crawl(array $annotations, LinkType $link, array &$body) : void
187
    {
188 4
        foreach ($annotations as $annotation) {
189
            /* @var $annotation Link */
190 4
            if (! $annotation instanceof Link || $annotation->crawl !== $link->key) {
191
                continue;
192 4
            }
193 4
            $uri = uri_template($annotation->href, $body);
194 4
            $rel = $this->factory->newInstance($uri);
195 4
            /* @noinspection UnnecessaryParenthesesInspection */
196 4
            $query = (new Uri($uri))->query;
197 4
            $request = new Request($this->invoker, $rel, Request::GET, $query, [$link], $this);
198
            $hash = $request->hash();
199 4
            if (array_key_exists($hash, $this->cache)) {
200
                /** @var array<mixed> */
201
                $body[$annotation->rel] = $this->cache[$hash];
202 4
203
                continue;
204 4
            }
205 4
            $this->cache[$hash] = $body[$annotation->rel] = $this->getResponseBody($request);
206
        }
207
    }
208 4
209 4
    /**
210 4
     * @return array<mixed>
211
     */
212
    private function getResponseBody(Request $request) : ?array
213
    {
214 4
        $body = $this->invoke($request)->body;
215
        assert(is_array($body) || $body === null);
216
217 4
        return $body;
218
    }
219 4
220
    /**
221
     * @param mixed $value
222 4
     */
223
    private function isList($value) : bool
224 4
    {
225
        if (! is_array($value)) {
226
            return false;
227
        }
228
        $list = $value;
229
        /** @var array<mixed> */
230
        $firstRow = array_pop($list);
231
        $keys = array_keys((array) $firstRow);
232
        /** @var array<array> $list */
233
        $isMultiColumnMultiRowList = $this->isMultiColumnMultiRowList($keys, $list);
234
        $isMultiColumnList = $this->isMultiColumnList($value, $firstRow);
235
        $isSingleColumnList = $this->isSingleColumnList($value, $keys, $list);
236
237
        return $isSingleColumnList || $isMultiColumnMultiRowList || $isMultiColumnList;
238
    }
239
240
    /**
241
     * @param array<int, int|string> $keys
242
     * @param array<array>           $list
243
     */
244
    private function isMultiColumnMultiRowList(array $keys, array $list) : bool
245
    {
246
        if ($keys === [0 => 0]) {
247
            return false;
248
        }
249
        foreach ($list as $item) {
250
            if ($keys !== array_keys((array) $item)) {
251
                return false;
252
            }
253
        }
254
255
        return true;
256
    }
257
258
    /**
259
     * @param array<int|string, mixed> $value
260
     * @param mixed                    $firstRow
261
     */
262
    private function isMultiColumnList(array $value, $firstRow) : bool
263
    {
264
        return is_array($firstRow) && array_filter(array_keys($value), 'is_numeric') === array_keys($value);
265
    }
266
267
    /**
268
     * @param array<int|string, mixed> $value
269
     * @param list<array-key>          $keys
0 ignored issues
show
Documentation introduced by
The doc-type list<array-key> could not be parsed: Expected "|" or "end of type", but got "<" at position 4. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
270
     * @param array<mixed, mixed>      $list
271
     */
272
    private function isSingleColumnList(array $value, array $keys, array $list) : bool
273
    {
274
        return (count($value) === 1) && $keys === array_keys($list);
275
    }
276
}
277