HalRenderer   A
last analyzed

Complexity

Total Complexity 20

Size/Duplication

Total Lines 123
Duplicated Lines 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
eloc 50
dl 0
loc 123
rs 10
c 6
b 0
f 0
wmc 20

8 Methods

Rating   Name   Duplication   Size   Complexity  
A getHal() 0 8 2
A render() 0 6 1
A updateHeaders() 0 14 3
A __construct() 0 3 1
A valuate() 0 19 4
A valuateElements() 0 27 6
A isDifferentSchema() 0 3 1
A renderHal() 0 11 2
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
The doc comment array<array-key, mixed> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key, mixed>.
Loading history...
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
Documentation Bug introduced by
The doc comment array{0: ResourceObject,...rray<array-key, mixed>} at position 10 could not be parsed: Unknown type name 'array-key' at position 10 in array{0: ResourceObject, 1: array<array-key, mixed>}.
Loading history...
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