Completed
Pull Request — master (#52)
by John
07:37
created

RefResolver::resolveRecursively()   D

Complexity

Conditions 10
Paths 32

Size

Total Lines 34
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 34
rs 4.8196
cc 10
eloc 23
nc 32
nop 3

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/*
3
 * This file is part of the KleijnWeb\SwaggerBundle package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
9
namespace KleijnWeb\SwaggerBundle\Document;
10
11
use KleijnWeb\SwaggerBundle\Document\Exception\ResourceNotReadableException;
12
use KleijnWeb\SwaggerBundle\Document\Exception\InvalidReferenceException;
13
14
/**
15
 * @author John Kleijn <[email protected]>
16
 */
17
class RefResolver
18
{
19
    /**
20
     * @var object
21
     */
22
    private $document;
23
24
    /**
25
     * @var string
26
     */
27
    private $uri;
28
29
    /**
30
     * @var string
31
     */
32
    private $directory;
33
34
    /**
35
     * @var YamlParser
36
     */
37
    private $yamlParser;
38
39
    /**
40
     * @param object     $document
41
     * @param string     $uri
42
     * @param YamlParser $yamlParser
43
     */
44
    public function __construct($document, $uri, YamlParser $yamlParser = null)
45
    {
46
        if (!is_object($document)) {
47
            throw new \InvalidArgumentException("Document must be object");
48
        }
49
50
        $this->document = $document;
51
        $uriSegs = $this->parseUri($uri);
52
        if (!$uriSegs['proto']) {
53
            $uri = realpath($uri);
54
        }
55
        $this->uri = $uri;
56
        $this->directory = dirname($this->uri);
57
        $this->yamlParser = $yamlParser ?: new YamlParser();
58
    }
59
60
    /**
61
     * @return object
62
     */
63
    public function getDocument()
64
    {
65
        return $this->document;
66
    }
67
68
    /**
69
     * Resolve all references
70
     *
71
     * @return object
72
     */
73
    public function resolve()
74
    {
75
        $this->resolveRecursively($this->document);
76
77
        return $this->document;
78
    }
79
80
    /**
81
     * Revert to original state
82
     */
83
    public function unresolve()
84
    {
85
        $this->unresolveRecursively($this->document, $this->document);
86
    }
87
88
    /**
89
     * @param object|array $composite
90
     * @param object       $document
91
     * @param string       $uri
92
     *
93
     * @throws InvalidReferenceException
94
     * @throws ResourceNotReadableException
95
     */
96
    private function resolveRecursively(&$composite, $document = null, $uri = null)
97
    {
98
        $document = $document ?: $this->document;
99
        $uri = $uri ?: $this->uri;
100
101
        if (is_array($composite)) {
102
            foreach ($composite as &$value) {
103
                if (!is_scalar($value)) {
104
                    $this->resolveRecursively($value, $document, $uri);
105
                }
106
            }
107
        } elseif (is_object($composite)) {
108
            if (property_exists($composite, '$ref')) {
109
                $uri = $composite->{'$ref'};
110
                if ('#' === $uri[0]) {
111
                    $composite = $this->lookup($uri, $document, $uri);
112
                } else {
113
                    $uriSegs = $this->parseUri($uri);
114
                    $normalizedUri = $this->normalizeUri($uriSegs);
115
                    $externalDocument = $this->loadExternal($normalizedUri);
116
                    $composite = $this->lookup($uriSegs['segment'], $externalDocument, $normalizedUri);
117
                    $this->resolveRecursively($composite, $externalDocument, $normalizedUri);
118
                }
119
120
                $composite->id = $uri;
121
                $composite->{'x-ref-id'} = $uri;
122
123
                return;
124
            }
125
            foreach ($composite as $propertyName => &$propertyValue) {
126
                $this->resolveRecursively($propertyValue, $document, $uri);
127
            }
128
        }
129
    }
130
131
    /**
132
     * @param object $current
133
     * @param object $parent
134
     *
135
     * @return void
136
     */
137
    private function unresolveRecursively($current, &$parent = null)
138
    {
139
        foreach ($current as $key => &$value) {
140
            if (is_object($value)) {
141
                $this->unresolveRecursively($value, $current);
142
            }
143
            if ($key === 'x-ref-id') {
144
                $parent = (object)['$ref' => $value];
145
            }
146
        }
147
    }
148
149
    /**
150
     * @param string $path
151
     * @param object $document
152
     * @param string $uri
153
     *
154
     * @return mixed
155
     * @throws InvalidReferenceException
156
     */
157
    private function lookup($path, $document, $uri)
158
    {
159
        $target = $this->lookupRecursively(
160
            explode('/', trim($path, '/#')),
161
            $document
162
        );
163
        if (!$target) {
164
            throw new InvalidReferenceException("Target '$path' does not exist' at '$uri''");
165
        }
166
167
        return $target;
168
    }
169
170
    /**
171
     * @param array  $segments
172
     * @param object $context
173
     *
174
     * @return mixed
175
     */
176
    private function lookupRecursively(array $segments, $context)
177
    {
178
        $segment = array_shift($segments);
179
        if (property_exists($context, $segment)) {
180
            if (!count($segments)) {
181
                return $context->$segment;
182
            }
183
184
            return $this->lookupRecursively($segments, $context->$segment);
185
        }
186
187
        return null;
188
    }
189
190
    /**
191
     * @param string $uri
192
     *
193
     * @return object
194
     * @throws ResourceNotReadableException
195
     */
196
    private function loadExternal($uri)
197
    {
198
        $exception = new ResourceNotReadableException("Failed reading '$uri'");
199
200
        set_error_handler(function () use ($exception) {
201
            throw $exception;
202
        });
203
        $response = file_get_contents($uri);
204
        restore_error_handler();
205
206
        if (false === $response) {
207
            throw $exception;
208
        }
209
        if (preg_match('/\b(yml|yaml)\b/', $uri)) {
210
            return $this->yamlParser->parse($response);
211
        }
212
213
        return json_decode($response);
214
    }
215
216
217
    /**
218
     * @param array $uriSegs
219
     *
220
     * @return string
221
     */
222
    private function normalizeUri(array $uriSegs)
223
    {
224
        return
225
            $uriSegs['proto'] . $uriSegs['host']
226
            . rtrim($uriSegs['root'], '/') . '/'
227
            . (!$uriSegs['root'] ? ltrim("$this->directory/", '/') : '')
228
            . $uriSegs['path'];
229
    }
230
231
    /**
232
     * @param string $uri
233
     *
234
     * @return array
235
     */
236
    private function parseUri($uri)
237
    {
238
        $defaults = [
239
            'root'    => '',
240
            'proto'   => '',
241
            'host'    => '',
242
            'path'    => '',
243
            'segment' => ''
244
        ];
245
        $pattern = '@'
246
            . '(?P<proto>[a-z]+\://)?'
247
            . '(?P<host>[0-9a-z\.\@\:]+\.[a-z]+)?'
248
            . '(?P<root>/)?'
249
            . '(?P<path>[^#]*)'
250
            . '(?P<segment>#.*)?'
251
            . '@';
252
253
        preg_match($pattern, $uri, $matches);
254
255
        return array_merge($defaults, array_intersect_key($matches, $defaults));
256
    }
257
}
258