Issues (141)

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
            $view = $this->render($embeded());
99
            $ro->body['_embedded'][$key] = json_decode($view, null, 512, JSON_THROW_ON_ERROR);
100
        }
101
    }
102
103
    /** @codeCoverageIgnore */
104
    private function isDifferentSchema(ResourceObject $parentRo, ResourceObject $childRo): bool
105
    {
106
        return $parentRo->uri->scheme . $parentRo->uri->host !== $childRo->uri->scheme . $childRo->uri->host;
107
    }
108
109
    /**
110
     * @param Body          $body
111
     * @param array<object> $annotations
112
     */
113
    private function getHal(AbstractUri $uri, array $body, array $annotations): Hal
114
    {
115
        $query = $uri->query ? '?' . http_build_query($uri->query) : '';
116
        $path = $uri->path . $query;
117
        $selfLink = $this->linker->getReverseLink($path, $uri->query);
118
        $hal = new Hal($selfLink, $body);
119
120
        return $this->linker->addHalLink($body, array_values($annotations), $hal);
121
    }
122
123
    /** @return ResourceObjectBody */
124
    private function valuate(ResourceObject $ro): array
125
    {
126
        if (is_scalar($ro->body)) {
127
            $ro->body = ['value' => $ro->body];
128
        }
129
130
        if ($ro->body === null) {
131
            $ro->body = [];
132
        }
133
134
        if (is_object($ro->body)) {
135
            $ro->body = (array) $ro->body;
136
        }
137
138
        // evaluate all request in body.
139
        $this->valuateElements($ro);
140
        assert(is_array($ro->body));
141
142
        return [$ro, $ro->body];
143
    }
144
145
    private function updateHeaders(ResourceObject $ro): void
146
    {
147
        $ro->headers['Content-Type'] = 'application/hal+json';
148
        if (! isset($ro->headers['Location'])) {
149
            return;
150
        }
151
152
        $url = parse_url($ro->headers['Location'], PHP_URL_QUERY);
153
        $isRelativePath = $url === null;
154
        $path = $isRelativePath ? $ro->headers['Location'] : $url;
155
        parse_str((string) $path, $query);
156
        /** @var Query $query */
157
158
        $ro->headers['Location'] = $this->linker->getReverseLink($ro->headers['Location'], $query);
159
    }
160
}
161