HalRenderer::valuateElements()   A
last analyzed

Complexity

Conditions 6
Paths 10

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
eloc 15
c 2
b 0
f 0
nc 10
nop 1
dl 0
loc 27
rs 9.2222
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 class HalRenderer implements RenderInterface
35
{
36
    public function __construct(
37
        private readonly HalLinker $linker,
38
    ) {
39
    }
40
41
    /**
42
     * {@inheritDoc}
43
     */
44
    #[Override]
45
    public function render(ResourceObject $ro)
46
    {
47
        $this->renderHal($ro);
48
        $this->updateHeaders($ro);
49
50
        return (string) $ro->view;
51
    }
52
53
    /**
54
     * {@inheritDoc}
55
     *
56
     * @throws RuntimeException
57
     */
58
    public function renderHal(ResourceObject $ro): void
59
    {
60
        [$ro, $body] = $this->valuate($ro);
61
        $method = 'on' . ucfirst($ro->uri->method);
62
        $hasMethod = method_exists($ro, $method);
63
        $annotations = $hasMethod ? (new ReflectionMethod($ro, $method))->getAnnotations() : [];
64
        $hal = $this->getHal($ro->uri, $body, $annotations);
65
        $json = $hal->asJson(true);
66
        assert(is_string($json));
67
        $ro->view = $json . PHP_EOL;
68
        $ro->headers['Content-Type'] = 'application/hal+json';
69
    }
70
71
    private function valuateElements(ResourceObject $ro): void
72
    {
73
        assert(is_array($ro->body));
74
        /** @var mixed $embeded */
75
        foreach ($ro->body as $key => &$embeded) {
76
            if (! ($embeded instanceof AbstractRequest)) {
77
                continue;
78
            }
79
80
            $isNotArray = ! isset($ro->body['_embedded']) || ! is_array($ro->body['_embedded']);
81
            if ($isNotArray) {
82
                $ro->body['_embedded'] = [];
83
            }
84
85
            assert(is_array($ro->body['_embedded']));
86
            // @codeCoverageIgnoreStart
87
            if ($this->isDifferentSchema($ro, $embeded->resourceObject)) {
88
                $ro->body['_embedded'][$key] = $embeded()->body;
89
                unset($ro->body[$key]);
90
91
                continue;
92
            }
93
94
            // @codeCoverageIgnoreEnd
95
            unset($ro->body[$key]);
96
            $view = $this->render($embeded());
97
            $ro->body['_embedded'][$key] = json_decode($view, null, 512, JSON_THROW_ON_ERROR);
98
        }
99
    }
100
101
    /** @codeCoverageIgnore */
102
    private function isDifferentSchema(ResourceObject $parentRo, ResourceObject $childRo): bool
103
    {
104
        return $parentRo->uri->scheme . $parentRo->uri->host !== $childRo->uri->scheme . $childRo->uri->host;
105
    }
106
107
    /**
108
     * @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...
109
     * @param array<object> $annotations
110
     */
111
    private function getHal(AbstractUri $uri, array $body, array $annotations): Hal
112
    {
113
        $query = $uri->query ? '?' . http_build_query($uri->query) : '';
114
        $path = $uri->path . $query;
115
        $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

115
        $selfLink = $this->linker->getReverseLink($path, /** @scrutinizer ignore-type */ $uri->query);
Loading history...
116
        $hal = new Hal($selfLink, $body);
117
118
        return $this->linker->addHalLink($body, array_values($annotations), $hal);
119
    }
120
121
    /** @return ResourceObjectBody */
0 ignored issues
show
Bug introduced by
The type BEAR\Resource\ResourceObjectBody 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...
122
    private function valuate(ResourceObject $ro): array
123
    {
124
        if (is_scalar($ro->body)) {
125
            $ro->body = ['value' => $ro->body];
126
        }
127
128
        if ($ro->body === null) {
129
            $ro->body = [];
130
        }
131
132
        if (is_object($ro->body)) {
133
            $ro->body = (array) $ro->body;
134
        }
135
136
        // evaluate all request in body.
137
        $this->valuateElements($ro);
138
        assert(is_array($ro->body));
139
140
        return [$ro, $ro->body];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array($ro, $ro->body) returns the type array<integer,BEAR\Resource\ResourceObject|array> which is incompatible with the documented return type BEAR\Resource\ResourceObjectBody.
Loading history...
141
    }
142
143
    private function updateHeaders(ResourceObject $ro): void
144
    {
145
        $ro->headers['Content-Type'] = 'application/hal+json';
146
        if (! isset($ro->headers['Location'])) {
147
            return;
148
        }
149
150
        $url = parse_url($ro->headers['Location'], PHP_URL_QUERY);
151
        $isRelativePath = $url === null;
152
        $path = $isRelativePath ? $ro->headers['Location'] : $url;
153
        parse_str((string) $path, $query);
154
        /** @var Query $query */
155
156
        $ro->headers['Location'] = $this->linker->getReverseLink($ro->headers['Location'], $query);
0 ignored issues
show
Bug introduced by
$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

156
        $ro->headers['Location'] = $this->linker->getReverseLink($ro->headers['Location'], /** @scrutinizer ignore-type */ $query);
Loading history...
157
    }
158
}
159