HalRenderer::valuate()   A
last analyzed

Complexity

Conditions 4
Paths 8

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
eloc 9
c 2
b 0
f 0
nc 8
nop 1
dl 0
loc 19
ccs 0
cts 0
cp 0
crap 20
rs 9.9666
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 1
use function json_decode;
19
use function method_exists;
20 1
use function parse_str;
21 1
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 1
/** @psalm-import-type Body from Types */
29
final class HalRenderer implements RenderInterface
30 1
{
31
    public function __construct(
32 1
        private readonly HalLinker $linker,
33 1
    ) {
34 1
    }
35
36 1
    /**
37 1
     * {@inheritDoc}
38 1
     */
39
    #[Override]
40 1
    public function render(ResourceObject $ro)
41
    {
42
        $this->renderHal($ro);
43 1
        $this->updateHeaders($ro);
44
45 1
        return (string) $ro->view;
46 1
    }
47
48
    /**
49
     * {@inheritDoc}
50
     *
51
     * @throws RuntimeException
52
     */
53
    public function renderHal(ResourceObject $ro): void
54
    {
55
        [$ro, $body] = $this->valuate($ro);
56 1
        $method = 'on' . ucfirst($ro->uri->method);
57
        $hasMethod = method_exists($ro, $method);
58
        $annotations = $hasMethod ? (new ReflectionMethod($ro, $method))->getAnnotations() : [];
59 1
        $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 1
        /** @var mixed $embeded */
70
        foreach ($ro->body as $key => &$embeded) {
71 1
            if (! ($embeded instanceof AbstractRequest)) {
72
                continue;
73
            }
74 1
75
            $isNotArray = ! isset($ro->body['_embedded']) || ! is_array($ro->body['_embedded']);
76 1
            if ($isNotArray) {
77 1
                $ro->body['_embedded'] = [];
78 1
            }
79 1
80 1
            assert(is_array($ro->body['_embedded']));
81
            // @codeCoverageIgnoreStart
82 1
            if ($this->isDifferentSchema($ro, $embeded->resourceObject)) {
83
                $ro->body['_embedded'][$key] = $embeded()->body;
84
                unset($ro->body[$key]);
85 1
86
                continue;
87
            }
88 1
89 1
            // @codeCoverageIgnoreEnd
90
            unset($ro->body[$key]);
91
            $view = $this->render($embeded());
92 1
            $ro->body['_embedded'][$key] = json_decode($view, null, 512, JSON_THROW_ON_ERROR);
93 1
        }
94
    }
95
96
    /** @codeCoverageIgnore */
97
    private function isDifferentSchema(ResourceObject $parentRo, ResourceObject $childRo): bool
98
    {
99 1
        return $parentRo->uri->scheme . $parentRo->uri->host !== $childRo->uri->scheme . $childRo->uri->host;
100
    }
101
102 1
    /**
103
     * @param Body $body
0 ignored issues
show
Bug introduced by
The type BEAR\Resource\Body was not found. Maybe you did not declare it correctly or list all dependencies?

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:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
104 1
     * @psalm-param list<object>       $annotations
105 1
     * @phpstan-param array<object>    $annotations
106 1
     */
107 1
    private function getHal(AbstractUri $uri, array $body, array $annotations): Hal
108 1
    {
109
        $query = $uri->query ? '?' . http_build_query($uri->query) : '';
110
        $path = $uri->path . $query;
111 1
        $selfLink = $this->linker->getReverseLink($path, $uri->query);
0 ignored issues
show
Bug introduced by
$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 ignore-type  annotation

111
        $selfLink = $this->linker->getReverseLink($path, /** @scrutinizer ignore-type */ $uri->query);
Loading history...
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
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...
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