Completed
Pull Request — master (#52)
by John
06:05
created

RefResolver::getDefinition()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
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 $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) {
1 ignored issue
show
Bug introduced by
The expression $current of type object|array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
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