1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace BEAR\Resource; |
||
6 | |||
7 | use Nocarrier\Hal; |
||
8 | use Ray\Aop\ReflectionMethod; |
||
9 | use RuntimeException; |
||
10 | |||
11 | use function assert; |
||
12 | use function http_build_query; |
||
13 | use function is_array; |
||
14 | use function is_object; |
||
15 | use function is_scalar; |
||
16 | use function is_string; |
||
17 | use function json_decode; |
||
18 | use function method_exists; |
||
19 | use function parse_str; |
||
20 | use function parse_url; |
||
21 | use function ucfirst; |
||
22 | |||
23 | use const JSON_THROW_ON_ERROR; |
||
24 | use const PHP_EOL; |
||
25 | use const PHP_URL_QUERY; |
||
26 | |||
27 | final class HalRenderer implements RenderInterface |
||
28 | { |
||
29 | public function __construct( |
||
30 | private readonly HalLinker $linker, |
||
31 | ) { |
||
32 | } |
||
33 | |||
34 | /** |
||
35 | * {@inheritDoc} |
||
36 | */ |
||
37 | public function render(ResourceObject $ro) |
||
38 | { |
||
39 | $this->renderHal($ro); |
||
40 | $this->updateHeaders($ro); |
||
41 | |||
42 | return (string) $ro->view; |
||
43 | } |
||
44 | |||
45 | /** |
||
46 | * {@inheritDoc} |
||
47 | * |
||
48 | * @throws RuntimeException |
||
49 | */ |
||
50 | public function renderHal(ResourceObject $ro): void |
||
51 | { |
||
52 | [$ro, $body] = $this->valuate($ro); |
||
53 | $method = 'on' . ucfirst($ro->uri->method); |
||
54 | $hasMethod = method_exists($ro, $method); |
||
55 | $annotations = $hasMethod ? (new ReflectionMethod($ro, $method))->getAnnotations() : []; |
||
56 | $hal = $this->getHal($ro->uri, $body, $annotations); |
||
57 | $json = $hal->asJson(true); |
||
58 | assert(is_string($json)); |
||
59 | $ro->view = $json . PHP_EOL; |
||
60 | $ro->headers['Content-Type'] = 'application/hal+json'; |
||
61 | } |
||
62 | |||
63 | private function valuateElements(ResourceObject $ro): void |
||
64 | { |
||
65 | assert(is_array($ro->body)); |
||
66 | /** @var mixed $embeded */ |
||
67 | foreach ($ro->body as $key => &$embeded) { |
||
68 | if (! ($embeded instanceof AbstractRequest)) { |
||
69 | continue; |
||
70 | } |
||
71 | |||
72 | $isNotArray = ! isset($ro->body['_embedded']) || ! is_array($ro->body['_embedded']); |
||
73 | if ($isNotArray) { |
||
74 | $ro->body['_embedded'] = []; |
||
75 | } |
||
76 | |||
77 | assert(is_array($ro->body['_embedded'])); |
||
78 | // @codeCoverageIgnoreStart |
||
79 | if ($this->isDifferentSchema($ro, $embeded->resourceObject)) { |
||
80 | $ro->body['_embedded'][$key] = $embeded()->body; |
||
81 | unset($ro->body[$key]); |
||
82 | |||
83 | continue; |
||
84 | } |
||
85 | |||
86 | // @codeCoverageIgnoreEnd |
||
87 | unset($ro->body[$key]); |
||
88 | $view = $this->render($embeded()); |
||
89 | $ro->body['_embedded'][$key] = json_decode($view, null, 512, JSON_THROW_ON_ERROR); |
||
90 | } |
||
91 | } |
||
92 | |||
93 | /** @codeCoverageIgnore */ |
||
94 | private function isDifferentSchema(ResourceObject $parentRo, ResourceObject $childRo): bool |
||
95 | { |
||
96 | return $parentRo->uri->scheme . $parentRo->uri->host !== $childRo->uri->scheme . $childRo->uri->host; |
||
97 | } |
||
98 | |||
99 | /** |
||
100 | * @param array<array-key, mixed> $body |
||
0 ignored issues
–
show
Documentation
Bug
introduced
by
![]() |
|||
101 | * @psalm-param list<object> $annotations |
||
102 | * @phpstan-param array<object> $annotations |
||
103 | */ |
||
104 | private function getHal(AbstractUri $uri, array $body, array $annotations): Hal |
||
105 | { |
||
106 | $query = $uri->query ? '?' . http_build_query($uri->query) : ''; |
||
107 | $path = $uri->path . $query; |
||
108 | $selfLink = $this->linker->getReverseLink($path, $uri->query); |
||
109 | $hal = new Hal($selfLink, $body); |
||
110 | |||
111 | return $this->linker->addHalLink($body, $annotations, $hal); |
||
112 | } |
||
113 | |||
114 | /** @return array{0: ResourceObject, 1: array<array-key, mixed>} */ |
||
0 ignored issues
–
show
|
|||
115 | private function valuate(ResourceObject $ro): array |
||
116 | { |
||
117 | if (is_scalar($ro->body)) { |
||
118 | $ro->body = ['value' => $ro->body]; |
||
119 | } |
||
120 | |||
121 | if ($ro->body === null) { |
||
122 | $ro->body = []; |
||
123 | } |
||
124 | |||
125 | if (is_object($ro->body)) { |
||
126 | $ro->body = (array) $ro->body; |
||
127 | } |
||
128 | |||
129 | // evaluate all request in body. |
||
130 | $this->valuateElements($ro); |
||
131 | assert(is_array($ro->body)); |
||
132 | |||
133 | return [$ro, $ro->body]; |
||
134 | } |
||
135 | |||
136 | private function updateHeaders(ResourceObject $ro): void |
||
137 | { |
||
138 | $ro->headers['Content-Type'] = 'application/hal+json'; |
||
139 | if (! isset($ro->headers['Location'])) { |
||
140 | return; |
||
141 | } |
||
142 | |||
143 | $url = parse_url($ro->headers['Location'], PHP_URL_QUERY); |
||
144 | $isRelativePath = $url === null; |
||
145 | $path = $isRelativePath ? $ro->headers['Location'] : $url; |
||
146 | parse_str((string) $path, $query); |
||
147 | /** @var array<string, string> $query */ |
||
148 | |||
149 | $ro->headers['Location'] = $this->linker->getReverseLink($ro->headers['Location'], $query); |
||
150 | } |
||
151 | } |
||
152 |