Completed
Pull Request — master (#52)
by John
02:48
created

RefResolver::resolveRecursively()   C

Complexity

Conditions 12
Paths 40

Size

Total Lines 35
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
c 3
b 1
f 0
dl 0
loc 35
rs 5.1612
cc 12
eloc 24
nc 40
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 $definition;
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     $definition
41
     * @param string     $uri
42
     * @param YamlParser $yamlParser
43
     */
44
    public function __construct($definition, $uri, YamlParser $yamlParser = null)
45
    {
46
        $this->definition = $definition;
47
        $uriSegs = $this->parseUri($uri);
48
        if (!$uriSegs['proto']) {
49
            $uri = realpath($uri);
50
        }
51
        $this->uri = $uri;
52
        $this->directory = dirname($this->uri);
53
        $this->yamlParser = $yamlParser ?: new YamlParser();
54
    }
55
56
    /**
57
     * @return object
58
     */
59
    public function getDefinition()
60
    {
61
        return $this->definition;
62
    }
63
64
    /**
65
     * Resolve all references
66
     *
67
     * @return object
68
     */
69
    public function resolve()
70
    {
71
        $this->resolveRecursively($this->definition);
72
73
        return $this->definition;
74
    }
75
76
    /**
77
     * Revert to original state
78
     *
79
     * @return object
80
     */
81
    public function unresolve()
82
    {
83
        $this->unresolveRecursively($this->definition);
84
85
        return $this->definition;
86
    }
87
88
    /**
89
     * @param object|array $current
90
     * @param object       $document
91
     * @param string       $uri
92
     *
93
     * @throws InvalidReferenceException
94
     * @throws ResourceNotReadableException
95
     */
96
    private function resolveRecursively(&$current, $document = null, $uri = null)
97
    {
98
        $document = $document ?: $this->definition;
99
        $uri = $uri ?: $this->uri;
100
101
        if (is_array($current)) {
102
            foreach ($current as &$value) {
103
                if ($value !== null && !is_scalar($value)) {
104
                    $this->resolveRecursively($value, $document, $uri);
105
                }
106
            }
107
        } elseif (is_object($current)) {
108
            if (property_exists($current, '$ref')) {
109
                $uri = $current->{'$ref'};
110
                if ('#' === $uri[0]) {
111
                    $current = $this->lookup($uri, $document);
112
                } else {
113
                    $uriSegs = $this->parseUri($uri);
114
                    $normalizedUri = $this->normalizeUri($uriSegs);
115
                    $externalDocument = $this->loadExternal($normalizedUri);
116
                    $current = $this->lookup($uriSegs['segment'], $externalDocument, $normalizedUri);
117
                    $this->resolveRecursively($current, $externalDocument, $normalizedUri);
118
                }
119
                if (is_object($current)) {
120
                    $current->id = $uri;
121
                    $current->{'x-ref-id'} = $uri;
122
                }
123
124
                return;
125
            }
126
            foreach ($current as $propertyName => &$propertyValue) {
127
                $this->resolveRecursively($propertyValue, $document, $uri);
128
            }
129
        }
130
    }
131
132
    /**
133
     * @param object|array $current
134
     * @param object|array $parent
135
     *
136
     * @return void
137
     */
138
    private function unresolveRecursively(&$current, &$parent = null)
139
    {
140
        foreach ($current as $key => &$value) {
141
            if ($value !== null && !is_scalar($value)) {
142
                $this->unresolveRecursively($value, $current);
143
            }
144
            if ($key === 'x-ref-id') {
145
                $parent = (object)['$ref' => $value];
146
            }
147
        }
148
    }
149
150
    /**
151
     * @param string $path
152
     * @param object $document
153
     * @param string $uri
154
     *
155
     * @return mixed
156
     * @throws InvalidReferenceException
157
     */
158
    private function lookup($path, $document, $uri = null)
159
    {
160
        $target = $this->lookupRecursively(
161
            explode('/', trim($path, '/#')),
162
            $document
163
        );
164
        if (!$target) {
165
            throw new InvalidReferenceException(
166
                "Target '$path' does not exist'" . ($uri ? " at '$uri''" : '')
167
            );
168
        }
169
170
        return $target;
171
    }
172
173
    /**
174
     * @param array  $segments
175
     * @param object $context
176
     *
177
     * @return mixed
178
     */
179
    private function lookupRecursively(array $segments, $context)
180
    {
181
        $segment = str_replace(['~0', '~1'], ['~', '/'], array_shift($segments));
182
        if (property_exists($context, $segment)) {
183
            if (!count($segments)) {
184
                return $context->$segment;
185
            }
186
187
            return $this->lookupRecursively($segments, $context->$segment);
188
        }
189
190
        return null;
191
    }
192
193
    /**
194
     * @param string $uri
195
     *
196
     * @return object
197
     * @throws ResourceNotReadableException
198
     */
199
    private function loadExternal($uri)
200
    {
201
        $exception = new ResourceNotReadableException("Failed reading '$uri'");
202
203
        set_error_handler(function () use ($exception) {
204
            throw $exception;
205
        });
206
        $response = file_get_contents($uri);
207
        restore_error_handler();
208
209
        if (false === $response) {
210
            throw $exception;
211
        }
212
        if (preg_match('/\b(yml|yaml)\b/', $uri)) {
213
            return $this->yamlParser->parse($response);
214
        }
215
216
        return json_decode($response);
217
    }
218
219
220
    /**
221
     * @param array $uriSegs
222
     *
223
     * @return string
224
     */
225
    private function normalizeUri(array $uriSegs)
226
    {
227
        return
228
            $uriSegs['proto'] . $uriSegs['host']
229
            . rtrim($uriSegs['root'], '/') . '/'
230
            . (!$uriSegs['root'] ? ltrim("$this->directory/", '/') : '')
231
            . $uriSegs['path'];
232
    }
233
234
    /**
235
     * @param string $uri
236
     *
237
     * @return array
238
     */
239
    private function parseUri($uri)
240
    {
241
        $defaults = [
242
            'root'    => '',
243
            'proto'   => '',
244
            'host'    => '',
245
            'path'    => '',
246
            'segment' => ''
247
        ];
248
        $pattern = '@'
249
            . '(?P<proto>[a-z]+\://)?'
250
            . '(?P<host>[0-9a-z\.\@\:]+\.[a-z]+)?'
251
            . '(?P<root>/)?'
252
            . '(?P<path>[^#]*)'
253
            . '(?P<segment>#.*)?'
254
            . '@';
255
256
        preg_match($pattern, $uri, $matches);
257
258
        return array_merge($defaults, array_intersect_key($matches, $defaults));
259
    }
260
}
261