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