Issues (158)

src/HalRenderer.php (1 issue)

Labels
Severity
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 array_values;
13
use function assert;
14
use function http_build_query;
15
use function is_array;
16
use function is_object;
17
use function is_scalar;
18
use function is_string;
19
use function json_decode;
20
use function method_exists;
21
use function parse_str;
22
use function parse_url;
23
use function ucfirst;
24
25
use const JSON_THROW_ON_ERROR;
26
use const PHP_EOL;
27
use const PHP_URL_QUERY;
28
29
/**
30
 * @psalm-import-type Body from Types
31
 * @psalm-import-type Query from Types
32
 * @psalm-import-type ResourceObjectBody from Types
33
 */
34
final readonly class HalRenderer implements RenderInterface
0 ignored issues
show
A parse error occurred: Syntax error, unexpected T_READONLY, expecting T_CLASS on line 34 at column 6
Loading history...
35
{
36
    public function __construct(
37
        private HalLinker $linker,
38
    ) {
39
    }
40
41
    /**
42
     * {@inheritDoc}
43
     *
44
     * @psalm-taint-escape html
45
     */
46
    #[Override]
47
    public function render(ResourceObject $ro)
48
    {
49
        $this->renderHal($ro);
50
        $this->updateHeaders($ro);
51
52
        return (string) $ro->view;
53
    }
54
55
    /**
56
     * {@inheritDoc}
57
     *
58
     * @throws RuntimeException
59
     */
60
    public function renderHal(ResourceObject $ro): void
61
    {
62
        [$ro, $body] = $this->valuate($ro);
63
        $method = 'on' . ucfirst($ro->uri->method);
64
        $hasMethod = method_exists($ro, $method);
65
        $annotations = $hasMethod ? (new ReflectionMethod($ro, $method))->getAnnotations() : [];
66
        $hal = $this->getHal($ro->uri, $body, $annotations);
67
        $json = $hal->asJson(true);
68
        assert(is_string($json));
69
        $ro->view = $json . PHP_EOL;
70
        $ro->headers['Content-Type'] = 'application/hal+json';
71
    }
72
73
    private function valuateElements(ResourceObject $ro): void
74
    {
75
        assert(is_array($ro->body));
76
        /** @var mixed $embeded */
77
        foreach ($ro->body as $key => &$embeded) {
78
            if (! ($embeded instanceof AbstractRequest)) {
79
                continue;
80
            }
81
82
            $isNotArray = ! isset($ro->body['_embedded']) || ! is_array($ro->body['_embedded']);
83
            if ($isNotArray) {
84
                $ro->body['_embedded'] = [];
85
            }
86
87
            assert(is_array($ro->body['_embedded']));
88
            // @codeCoverageIgnoreStart
89
            if ($this->isDifferentSchema($ro, $embeded->resourceObject)) {
90
                $ro->body['_embedded'][$key] = $embeded()->body;
91
                unset($ro->body[$key]);
92
93
                continue;
94
            }
95
96
            // @codeCoverageIgnoreEnd
97
            unset($ro->body[$key]);
98
            $embeddedRo = $embeded();
99
            $isHalCached = is_string($embeddedRo->view)
100
                && ($embeddedRo->headers['Content-Type'] ?? '') === 'application/hal+json';
101
            if ($isHalCached) {
102
                $ro->body['_embedded'][$key] = json_decode($embeddedRo->view, null, 512, JSON_THROW_ON_ERROR);
103
                continue;
104
            }
105
106
            $view = $this->render($embeddedRo);
107
            $ro->body['_embedded'][$key] = json_decode($view, null, 512, JSON_THROW_ON_ERROR);
108
        }
109
    }
110
111
    /** @codeCoverageIgnore */
112
    private function isDifferentSchema(ResourceObject $parentRo, ResourceObject $childRo): bool
113
    {
114
        return $parentRo->uri->scheme . $parentRo->uri->host !== $childRo->uri->scheme . $childRo->uri->host;
115
    }
116
117
    /**
118
     * @param Body          $body
119
     * @param array<object> $annotations
120
     */
121
    private function getHal(AbstractUri $uri, array $body, array $annotations): Hal
122
    {
123
        $query = $uri->query ? '?' . http_build_query($uri->query) : '';
124
        $path = $uri->path . $query;
125
        $selfLink = $this->linker->getReverseLink($path, $uri->query);
126
        $hal = new Hal($selfLink, $body);
127
128
        return $this->linker->addHalLink($body, array_values($annotations), $hal);
129
    }
130
131
    /** @return ResourceObjectBody */
132
    private function valuate(ResourceObject $ro): array
133
    {
134
        if (is_scalar($ro->body)) {
135
            $ro->body = ['value' => $ro->body];
136
        }
137
138
        if ($ro->body === null) {
139
            $ro->body = [];
140
        }
141
142
        if (is_object($ro->body)) {
143
            $ro->body = (array) $ro->body;
144
        }
145
146
        // evaluate all request in body.
147
        $this->valuateElements($ro);
148
        assert(is_array($ro->body));
149
150
        return [$ro, $ro->body];
151
    }
152
153
    private function updateHeaders(ResourceObject $ro): void
154
    {
155
        $ro->headers['Content-Type'] = 'application/hal+json';
156
        if (! isset($ro->headers['Location'])) {
157
            return;
158
        }
159
160
        $url = parse_url($ro->headers['Location'], PHP_URL_QUERY);
161
        $isRelativePath = $url === null;
162
        $path = $isRelativePath ? $ro->headers['Location'] : $url;
163
        parse_str((string) $path, $query);
164
        /** @var Query $query */
165
166
        $ro->headers['Location'] = $this->linker->getReverseLink($ro->headers['Location'], $query);
167
    }
168
}
169