Completed
Pull Request — master (#12)
by Jan
02:43
created

Resolver   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 288
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 99%

Importance

Changes 12
Bugs 1 Features 3
Metric Value
wmc 35
c 12
b 1
f 3
lcom 1
cbo 12
dl 0
loc 288
ccs 99
cts 100
cp 0.99
rs 9

11 Methods

Rating   Name   Duplication   Size   Complexity  
A setPreFetchHook() 0 4 1
A initialize() 0 9 3
A getRootUri() 0 8 2
A getRootSchema() 0 4 1
A getCurrentUri() 0 8 2
A enter() 0 10 3
A leave() 0 8 2
B resolve() 0 31 6
A registerSchema() 0 14 4
B fetchSchemaAt() 0 30 4
C resolvePointer() 0 34 7
1
<?php
2
3
/*
4
 * This file is part of the JVal package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
namespace JVal;
11
12
use JVal\Exception\JsonDecodeException;
13
use JVal\Exception\Resolver\EmptyStackException;
14
use JVal\Exception\Resolver\InvalidPointerIndexException;
15
use JVal\Exception\Resolver\InvalidPointerTargetException;
16
use JVal\Exception\Resolver\InvalidRemoteSchemaException;
17
use JVal\Exception\Resolver\InvalidSegmentTypeException;
18
use JVal\Exception\Resolver\SelfReferencingPointerException;
19
use JVal\Exception\Resolver\UnfetchableUriException;
20
use JVal\Exception\Resolver\UnresolvedPointerIndexException;
21
use JVal\Exception\Resolver\UnresolvedPointerPropertyException;
22
use Closure;
23
use stdClass;
24
25
/**
26
 * Resolves JSON pointer references within a schema. Handles local/remote
27
 * URIs, resolution scope alterations, and nested/recursive references.
28
 */
29
class Resolver
30
{
31
    /**
32
     * @var stdClass
33
     */
34
    private $rootSchema;
35
36
    /**
37
     * Stack of URIs used for resolving relative URIs.
38
     *
39
     * @var Uri[]
40
     */
41
    private $uriStack = [];
42
43
    /**
44
     * Schema cache. Each schema visited at a given URI is stored
45
     * in the cache to avoid superfluous requests.
46
     *
47
     * @var array
48
     */
49
    private $schemas = [];
50
51
    /**
52
     * @see setPreFetchHook
53
     *
54
     * @var Closure
55
     */
56
    private $preFetchHook;
57
58
    /**
59
     * Initializes the resolver with a root schema, on which resolutions will be based.
60
     *
61
     * @param stdClass $schema
62
     * @param Uri      $uri
63
     */
64 347
    public function initialize(stdClass $schema, Uri $uri)
65
    {
66 347
        if ($uri->isAbsolute() && !$uri->hasPointer()) {
67 337
            $this->registerSchema($schema, $uri);
68 337
        }
69
70 347
        $this->rootSchema = $schema;
71 347
        $this->uriStack = [$uri];
72 347
    }
73
74
    /**
75
     * Returns URI of root schema.
76
     *
77
     * @return Uri
78
     *
79
     * @throws EmptyStackException
80
     */
81 2
    public function getRootUri()
82
    {
83 2
        if (count($this->uriStack) === 0) {
84 1
            throw new EmptyStackException();
85
        }
86
87 1
        return reset($this->uriStack);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression reset($this->uriStack); of type JVal\Uri|false adds false to the return on line 87 which is incompatible with the return type documented by JVal\Resolver::getRootUri of type JVal\Uri. It seems like you forgot to handle an error condition.
Loading history...
88
    }
89
90
    /**
91
     * Returns root schema.
92
     *
93
     * @return stdClass|null
94
     */
95 11
    public function getRootSchema()
96
    {
97 11
        return $this->rootSchema;
98
    }
99
100
    /**
101
     * Returns the URI of the current schema.
102
     *
103
     * @return Uri
104
     *
105
     * @throws EmptyStackException
106
     */
107 68
    public function getCurrentUri()
108
    {
109 68
        if (count($this->uriStack) === 0) {
110 1
            throw new EmptyStackException();
111
        }
112
113 67
        return end($this->uriStack);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression end($this->uriStack); of type JVal\Uri|false adds false to the return on line 113 which is incompatible with the return type documented by JVal\Resolver::getCurrentUri of type JVal\Uri. It seems like you forgot to handle an error condition.
Loading history...
114
    }
115
116
    /**
117
     * Sets an URI pre-fetch hook. The hook function will be called each time
118
     * a remote reference is about to be fetched. It is passed the original
119
     * pointer URI and must return a new URI string.
120
     *
121
     * @param Closure $preFetchHook
122
     */
123 300
    public function setPreFetchHook(Closure $preFetchHook)
124
    {
125 300
        $this->preFetchHook = $preFetchHook;
126 300
    }
127
128
    /**
129
     * Pushes an URI and its associated schema onto the resolution stack,
130
     * making them the current URI/schema pair.
131
     *
132
     * @param Uri      $uri
133
     * @param stdClass $schema
134
     *
135
     * @throws EmptyStackException
136
     */
137 24
    public function enter(Uri $uri, stdClass $schema)
138
    {
139 24
        $currentUri = $this->getCurrentUri();
140 24
        $resolvedUri = $uri->resolveAgainst($currentUri);
141 24
        $this->uriStack[] = $resolvedUri;
142
143 24
        if ($resolvedUri->isAbsolute() && !$resolvedUri->hasPointer()) {
144 14
            $this->registerSchema($schema, $resolvedUri);
145 14
        }
146 24
    }
147
148
    /**
149
     * Removes the URI/schema pair at the top of the resolution stack,
150
     * thus returning to the previous URI/schema context.
151
     *
152
     * @throws EmptyStackException
153
     */
154 24
    public function leave()
155
    {
156 24
        if (count($this->uriStack) === 0) {
157 1
            throw new EmptyStackException();
158
        }
159
160 23
        array_pop($this->uriStack);
161 23
    }
162
163
    /**
164
     * Resolves a schema reference according to the JSON Reference
165
     * specification draft. Returns an array containing the resolved
166
     * URI and the resolved schema.
167
     *
168
     * @param stdClass $reference
169
     *
170
     * @throws InvalidPointerTargetException
171
     * @throws SelfReferencingPointerException
172
     *
173
     * @return array
174
     */
175 65
    public function resolve(stdClass $reference)
176
    {
177 65
        $baseUri = $this->getCurrentUri();
178 65
        $uri = new Uri($reference->{'$ref'});
179
180 65
        if ($baseUri->getPrimaryResourceIdentifier() === '' && $uri->getPrimaryResourceIdentifier() === '') {
181 10
            $schema = $this->getRootSchema();
182 10
        } else {
183 55
            $uri = $uri->resolveAgainst($baseUri);
184 55
            $identifier = $uri->getPrimaryResourceIdentifier();
185
186 55
            if (isset($this->schemas[$identifier])) {
187 38
                $schema = $this->schemas[$identifier];
188 38
            } else {
189 23
                $schema = $this->fetchSchemaAt($identifier);
190 16
                $this->registerSchema($schema, $uri);
191
            }
192
        }
193
194 58
        $resolved = $this->resolvePointer($schema, $uri);
195
196 49
        if ($resolved === $reference) {
197 2
            throw new SelfReferencingPointerException();
198
        }
199
200 47
        if (!is_object($resolved)) {
201 2
            throw new InvalidPointerTargetException([$uri->getRawUri()]);
202
        }
203
204 45
        return [$uri, $resolved];
205
    }
206
207
    /**
208
     * Registers a schema reference for future use.
209
     *
210
     * @param stdClass $schema
211
     * @param Uri      $uri
212
     */
213 339
    public function registerSchema(stdClass $schema, Uri $uri)
214
    {
215 339
        if (!$uri->isAbsolute()) {
216
            throw new \LogicException('Unable to register schema without absolute URI');
217
        }
218
219 339
        $identifier = $uri->getPrimaryResourceIdentifier();
220
221 339
        if (!isset($this->schemas[$identifier])) {
222 339
            $this->schemas[$identifier] = $schema;
223 339
        } elseif (!Utils::areEqual($this->schemas[$identifier], $schema)) {
224 1
            throw new \LogicException('Different schema is already registered with given URI');
225
        }
226 339
    }
227
228
    /**
229
     * Fetches a remote schema and ensures it is valid.
230
     *
231
     * @param string $uri
232
     *
233
     * @throws InvalidRemoteSchemaException
234
     * @throws JsonDecodeException
235
     *
236
     * @return stdClass
237
     */
238 23
    private function fetchSchemaAt($uri)
239
    {
240 23
        if ($hook = $this->preFetchHook) {
241 14
            $uri = $hook($uri);
242 14
        }
243
244 23
        set_error_handler(function ($severity, $error) use ($uri) {
245 5
            restore_error_handler();
246 5
            throw new UnfetchableUriException([$uri, $error, $severity]);
247 23
        });
248
249 23
        $content = file_get_contents($uri);
250 18
        restore_error_handler();
251
252 18
        $schema = json_decode($content);
253
254 18
        if (json_last_error() !== JSON_ERROR_NONE) {
255 1
            throw new JsonDecodeException(sprintf(
256 1
                'Cannot decode JSON from URI "%s" (error: %s)',
257 1
                $uri,
258 1
                Utils::lastJsonErrorMessage()
259 1
            ));
260
        }
261
262 17
        if (!is_object($schema)) {
263 1
            throw new InvalidRemoteSchemaException([$uri]);
264
        }
265
266 16
        return $schema;
267
    }
268
269
    /**
270
     * Resolves a JSON pointer according to RFC 6901.
271
     *
272
     * @param stdClass $schema
273
     * @param Uri      $pointerUri
274
     *
275
     * @return mixed
276
     *
277
     * @throws InvalidPointerIndexException
278
     * @throws InvalidSegmentTypeException
279
     * @throws UnresolvedPointerIndexException
280
     * @throws UnresolvedPointerPropertyException
281
     */
282 58
    private function resolvePointer(stdClass $schema, Uri $pointerUri)
283
    {
284 58
        $segments = $pointerUri->getPointerSegments();
285 58
        $pointer = $pointerUri->getRawPointer();
286 58
        $currentNode = $schema;
287
288 58
        for ($i = 0, $max = count($segments); $i < $max; ++$i) {
289 47
            if (is_object($currentNode)) {
290 47
                if (property_exists($currentNode, $segments[$i])) {
291 46
                    $currentNode = $currentNode->{$segments[$i]};
292 46
                    continue;
293
                }
294
295 3
                throw new UnresolvedPointerPropertyException([$segments[$i], $i, $pointer]);
296
            }
297
298 12
            if (is_array($currentNode)) {
299 12
                if (!preg_match('/^\d+$/', $segments[$i])) {
300 2
                    throw new InvalidPointerIndexException([$segments[$i], $i, $pointer]);
301
                }
302
303 12
                if (!isset($currentNode[$index = (int) $segments[$i]])) {
304 2
                    throw new UnresolvedPointerIndexException([$segments[$i], $i, $pointer]);
305
                }
306
307 10
                $currentNode = $currentNode[$index];
308 10
                continue;
309
            }
310
311 2
            throw new InvalidSegmentTypeException([$i, $pointer]);
312
        }
313
314 49
        return $currentNode;
315
    }
316
}
317