Completed
Pull Request — master (#74)
by John
02:46
created

RefResolver::unresolve()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
c 3
b 1
f 0
dl 0
loc 6
rs 9.4285
cc 1
eloc 3
nc 1
nop 0
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 $loader;
38
39
    /**
40
     * @param object $definition
41
     * @param string $uri
42
     * @param Loader $loader
43
     */
44
    public function __construct($definition, $uri, Loader $loader = null)
45
    {
46
        $this->definition = $definition;
47
        $uriSegs          = $this->parseUri($uri);
48
        if (!$uriSegs['scheme']) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
49
            //$uri = realpath($uri);
0 ignored issues
show
Unused Code Comprehensibility introduced by
56% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
50
        }
51
        $this->uri       = $uri;
52
        $this->directory = dirname($this->uri);
53
        $this->loader    = $loader ?: new Loader();
0 ignored issues
show
Documentation Bug introduced by
It seems like $loader ?: new \KleijnWe...undle\Document\Loader() of type object<KleijnWeb\SwaggerBundle\Document\Loader> is incompatible with the declared type object<KleijnWeb\Swagger...le\Document\YamlParser> of property $loader.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
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
                $this->resolveRecursively($value, $document, $uri);
104
            }
105
        } elseif (is_object($current)) {
106
            if (property_exists($current, '$ref')) {
107
                $uri = $current->{'$ref'};
108
                if ('#' === $uri[0]) {
109
                    $current = $this->lookup($uri, $document);
110
                    $this->resolveRecursively($current, $document, $uri);
111
                } else {
112
                    $uriSegs          = $this->parseUri($uri);
113
                    $normalizedUri    = $this->normalizeFileUri($uriSegs);
114
                    $externalDocument = $this->loadExternal($normalizedUri);
115
                    $current          = $this->lookup($uriSegs['fragment'], $externalDocument, $normalizedUri);
116
                    $this->resolveRecursively($current, $externalDocument, $normalizedUri);
117
                }
118
                if (is_object($current)) {
119
                    $current->{'x-ref-id'} = $uri;
120
                }
121
122
                return;
123
            }
124
            foreach ($current as $propertyName => &$propertyValue) {
125
                $this->resolveRecursively($propertyValue, $document, $uri);
126
            }
127
        }
128
    }
129
130
    /**
131
     * @param object|array $current
132
     * @param object|array $parent
133
     *
134
     * @return void
135
     */
136
    private function unresolveRecursively(&$current, &$parent = null)
137
    {
138
        foreach ($current as $key => &$value) {
139
            if ($value !== null && !is_scalar($value)) {
140
                $this->unresolveRecursively($value, $current);
141
            }
142
            if ($key === 'x-ref-id') {
143
                $parent = (object)['$ref' => $value];
144
            }
145
        }
146
    }
147
148
    /**
149
     * @param string $path
150
     * @param object $document
151
     * @param string $uri
152
     *
153
     * @return mixed
154
     * @throws InvalidReferenceException
155
     */
156
    private function lookup($path, $document, $uri = null)
157
    {
158
        $target = $this->lookupRecursively(
159
            explode('/', trim($path, '/#')),
160
            $document
161
        );
162
        if (!$target) {
163
            throw new InvalidReferenceException(
164
                "Target '$path' does not exist'" . ($uri ? " at '$uri''" : '')
165
            );
166
        }
167
168
        return $target;
169
    }
170
171
    /**
172
     * @param array  $segments
173
     * @param object $context
174
     *
175
     * @return mixed
176
     */
177
    private function lookupRecursively(array $segments, $context)
178
    {
179
        $segment = str_replace(['~0', '~1'], ['~', '/'], array_shift($segments));
180
        if (property_exists($context, $segment)) {
181
            if (!count($segments)) {
182
                return $context->$segment;
183
            }
184
185
            return $this->lookupRecursively($segments, $context->$segment);
186
        }
187
188
        return null;
189
    }
190
191
    /**
192
     * @param string $fileUrl
193
     *
194
     * @return object
195
     */
196
    private function loadExternal($fileUrl)
197
    {
198
        return $this->loader->load($fileUrl);
0 ignored issues
show
Bug introduced by
The method load() does not seem to exist on object<KleijnWeb\Swagger...le\Document\YamlParser>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
199
    }
200
201
    /**
202
     * @param array $uriSegs
203
     *
204
     * @return string
205
     */
206
    private function normalizeFileUri(array $uriSegs)
207
    {
208
        $path  = $uriSegs['path'];
209
        $auth  = !$uriSegs['user'] ? '' : "{$uriSegs['user']}:{$uriSegs['pass']}@";
210
        $query = !$uriSegs['query'] ? '' : "?{$uriSegs['query']}";
211
        $port  = !$uriSegs['port'] ? '' : ":{$uriSegs['port']}";
212
        $host  = !$uriSegs['host'] ? '' : "{$uriSegs['scheme']}://$auth{$uriSegs['host']}{$port}";
213
214
        if (substr($path, 0, 1) !== '/') {
215
            $path = "$this->directory/$path";
216
            if (substr($this->directory, 0, 1) === '/') {
217
                //Assume working directory is web root
218
                $path = ltrim($path, '/');
219
            }
220
        }
221
222
        return "{$host}{$path}{$query}";
223
    }
224
225
    /**
226
     * @param string $uri
227
     *
228
     * @return array
229
     */
230
    private function parseUri($uri)
231
    {
232
        $defaults = [
233
            'scheme'   => '',
234
            'host'     => '',
235
            'port'     => '',
236
            'user'     => '',
237
            'pass'     => '',
238
            'path'     => '',
239
            'query'    => '',
240
            'fragment' => ''
241
        ];
242
243
        if (0 === strpos($uri, 'file://')) {
244
            // parse_url botches this up
245
            preg_match('@file://(?P<path>[^#]*)(?P<fragment>#.*)?@', $uri, $matches);
246
247
            return array_merge($defaults, array_intersect_key($matches, $defaults));
248
        }
249
250
        return array_merge($defaults, array_intersect_key(parse_url($uri), $defaults));
251
    }
252
}
253