Completed
Push — 1.x ( 071ffc...c21892 )
by Akihito
04:09
created

HalRenderer   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 228
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 36
c 0
b 0
f 0
lcom 1
cbo 10
dl 0
loc 228
ccs 61
cts 61
cp 1
rs 8.8

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
C render() 0 26 7
A hasReturnCreatedResourceAnnotation() 0 10 3
A valuateElements() 0 16 4
A isDifferentSchema() 0 4 1
A getHal() 0 11 2
A getReverseMatchedLink() 0 15 4
A valuate() 0 16 4
B getHalLink() 0 19 6
A updateHeaders() 0 7 2
A getLocatedView() 0 12 2
1
<?php
2
/**
3
 * This file is part of the BEAR.Package package.
4
 *
5
 * @license http://opensource.org/licenses/MIT MIT
6
 */
7
namespace BEAR\Package\Provide\Representation;
8
9
use BEAR\Package\Annotation\Curies;
10
use BEAR\Package\Annotation\ReturnCreatedResource;
11
use BEAR\Package\Exception\LocationHeaderRequestException;
12
use BEAR\Resource\AbstractRequest;
13
use BEAR\Resource\AbstractUri;
14
use BEAR\Resource\Annotation\Link;
15
use BEAR\Resource\RenderInterface;
16
use BEAR\Resource\ResourceInterface;
17
use BEAR\Resource\ResourceObject;
18
use BEAR\Resource\Uri;
19
use BEAR\Sunday\Extension\Router\RouterInterface;
20
use Doctrine\Common\Annotations\Reader;
21
use Nocarrier\Hal;
22
23
/**
24
 * HAL(Hypertext Application Language) renderer
25
 */
26
class HalRenderer implements RenderInterface
27
{
28
    /**
29
     * @var Reader
30
     */
31
    private $reader;
32
33
    /**
34
     * @var RouterInterface
35
     */
36
    private $router;
37
38 8
    /**
39
     * @var ResourceInterface
40 8
     */
41 8
    private $resource;
42 8
43
    /**
44
     * @var Curies
45
     */
46
    private $curies;
47 7
48
    /**
49 7
     * @param Reader          $reader
50 7
     * @param RouterInterface $router
51 7
     */
52 7
    public function __construct(Reader $reader, RouterInterface $router, ResourceInterface $resource)
53 1
    {
54
        $this->reader = $reader;
55 1
        $this->router = $router;
56
        $this->resource = $resource;
57 6
    }
58
59
    /**
60 6
     * {@inheritdoc}
61 6
     */
62 6
    public function render(ResourceObject $ro)
63
    {
64 6
        $method = 'on' . ucfirst($ro->uri->method);
65
        $hasMethod = method_exists($ro, $method);
66
        if (! $hasMethod) {
67
            $ro->view = ''; // OPTIONS request no view
68
69
            return '';
70 3
        }
71
        $annotations = ($hasMethod) ? $this->reader->getMethodAnnotations(new \ReflectionMethod($ro, $method)) : [];
72 3
        $this->curies = $this->reader->getClassAnnotation(new \ReflectionClass($ro), Curies::class);
73 3
        $isReturnCreatedResource = $ro->code === 201 && isset($ro->headers['Location']) && $ro->uri->method === 'post' && $this->hasReturnCreatedResourceAnnotation($annotations);
74 1
        if ($isReturnCreatedResource) {
75 1
            $ro->view = $this->getLocatedView($ro);
76 1
77
            return $ro->view;
78
        }
79 3
        list($ro, $body) = $this->valuate($ro);
80
        /* @var $annotations Link[] */
81
        /* @var $ro ResourceObject */
82
        $hal = $this->getHal($ro->uri, $body, $annotations);
83
        $ro->view = $hal->asJson(true) . PHP_EOL;
84
        $this->updateHeaders($ro);
85
86
        return $ro->view;
87
    }
88 6
89
    /**
90 6
     * @return bool
91 6
     */
92 6
    private function hasReturnCreatedResourceAnnotation(array $annotations)
93 6
    {
94 6
        foreach ($annotations as $annotation) {
95
            if ($annotation instanceof ReturnCreatedResource) {
96 6
                return true;
97
            }
98
        }
99
100
        return false;
101
    }
102
103
    /**
104 6
     * @param \BEAR\Resource\ResourceObject $ro
105
     */
106 6
    private function valuateElements(ResourceObject &$ro)
107 6
    {
108 6
        foreach ($ro->body as $key => &$embeded) {
109 6
            if ($embeded instanceof AbstractRequest) {
110 3
                $isDefferentSchema = $this->isDifferentSchema($ro, $embeded->resourceObject);
111
                if ($isDefferentSchema === true) {
112 4
                    $ro->body['_embedded'][$key] = $embeded()->body;
113 4
                    unset($ro->body[$key]);
114 1
                    continue;
115
                }
116
                unset($ro->body[$key]);
117 3
                $view = $this->render($embeded());
118
                $ro->body['_embedded'][$key] = json_decode($view);
119
            }
120
        }
121
    }
122
123
    /**
124
     * Return is different schema (page <-> app)
125 7
     *
126
     * @param ResourceObject $parentRo
127
     * @param ResourceObject $childRo
128 7
     *
129 3
     * @return bool
130
     */
131
    private function isDifferentSchema(ResourceObject $parentRo, ResourceObject $childRo)
132 7
    {
133 7
        return $parentRo->uri->host . $parentRo->uri->host !== $childRo->uri->scheme . $childRo->uri->host;
134 1
    }
135
136 1
    /**
137
     * @param Uri   $uri
138
     * @param array $body
139 6
     * @param array $annotations
140
     *
141
     * @return Hal
142
     */
143
    private function getHal(AbstractUri $uri, array $body, array $annotations)
144
    {
145
        $query = $uri->query ? '?' . http_build_query($uri->query) : '';
146
        $path = $uri->path . $query;
147
        $selfLink = $this->getReverseMatchedLink($path);
148
149 6
        $hal = new Hal($selfLink, $body);
150
        $this->getHalLink($body, $annotations, $hal);
151 6
152 3
        return $hal;
153 1
    }
154
155 3
    /**
156 3
     * @param string $uri
157 3
     *
158
     * @return mixed
159 6
     */
160
    private function getReverseMatchedLink($uri)
161
    {
162
        $urlParts = parse_url($uri);
163
        $routeName = $urlParts['path'];
164 6
        isset($urlParts['query']) ? parse_str($urlParts['query'], $value) : $value = [];
165
        if ($value === []) {
166 6
            return $uri;
167 6
        }
168 1
        $reverseUri = $this->router->generate($routeName, (array) $value);
169
        if (is_string($reverseUri)) {
170 6
            return $reverseUri;
171
        }
172
173
        return $uri;
174
    }
175
176
    /**
177
     * @param ResourceObject $ro
178
     *
179
     * @return array [ResourceObject, array]
180
     */
181
    private function valuate(ResourceObject $ro)
182
    {
183
        // evaluate all request in body.
184
        if (is_array($ro->body)) {
185
            $this->valuateElements($ro);
186
        }
187
        // HAL
188
        $body = $ro->body ?: [];
189
        if (is_scalar($body)) {
190
            $body = ['value' => $body];
191
192
            return [$ro, $body];
193
        }
194
195
        return[$ro, (array) $body];
196
    }
197
198
    /**
199
     * @param array $body
200
     * @param array $methodAnnotations
201
     * @param Hal   $hal
202
     *
203
     * @internal param Uri $uri
204
     */
205
    private function getHalLink(array $body, array $methodAnnotations, Hal $hal)
206
    {
207
        if ($this->curies instanceof Curies) {
208
            $hal->addCurie($this->curies->name, $this->curies->href);
209
        }
210
        foreach ($methodAnnotations as $annotation) {
211
            if (! $annotation instanceof Link) {
212
                continue;
213
            }
214
            $uri = uri_template($annotation->href, $body);
215
            $reverseUri = $this->getReverseMatchedLink($uri);
216
            $hal->addLink($annotation->rel, $reverseUri);
217
        }
218
        if (isset($body['_links'])) {
219
            foreach ($body['_links'] as $rel => $annotation) {
220
                $hal->addLink($rel, $annotation);
221
            }
222
        }
223
    }
224
225
    /**
226
     * @param ResourceObject $ro
227
     */
228
    private function updateHeaders(ResourceObject $ro)
229
    {
230
        $ro->headers['content-type'] = 'application/hal+json';
231
        if (isset($ro->headers['Location'])) {
232
            $ro->headers['Location'] = $this->getReverseMatchedLink($ro->headers['Location']);
233
        }
234
    }
235
236
    /**
237
     * Return `Location` URI view
238
     *
239
     * @return string
240
     */
241
    private function getLocatedView(ResourceObject $ro)
242
    {
243
        $url = parse_url($ro->uri);
244
        $locationUri = sprintf('%s://%s%s', $url['scheme'], $url['host'], $ro->headers['Location']);
245
        try {
246
            $locatedResource = $this->resource->uri($locationUri)->eager->request();
0 ignored issues
show
Bug introduced by
Accessing eager on the interface BEAR\Resource\RequestInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
247
        } catch (\Exception $e) {
248
            throw new LocationHeaderRequestException($locationUri, 0, $e);
249
        }
250
251
        return $locatedResource->toString();
252
    }
253
}
254