Completed
Push — master ( 9a0752...0003b1 )
by Stéphane
8s
created

Resolver::resolve()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 30
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 30
ccs 18
cts 18
cp 1
rs 8.439
cc 5
eloc 17
nc 12
nop 1
crap 5
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
     * Schema resolution stack. Each item on the stack is an array
33
     * containing an uri and a schema.
34
     *
35
     * @var array
36
     */
37
    private $stack = [];
38
39
    /**
40
     * Schema cache. Each schema visited at a given URI is stored
41
     * in the cache to avoid superfluous requests.
42
     *
43
     * @var array
44
     */
45
    private $schemas = [];
46
47
    /**
48
     * @see setPreFetchHook
49
     *
50
     * @var Closure
51
     */
52
    private $preFetchHook;
53
54
    /**
55
     * Returns whether a base schema has been set.
56
     *
57
     * @return bool
58
     */
59
    public function hasBaseSchema()
60
    {
61
        return count($this->stack) > 0;
62
    }
63
64
    /**
65
     * Sets the current schema, on which resolutions will be based.
66
     *
67
     * @param stdClass $schema
68
     * @param Uri      $uri
69
     */
70 336
    public function setBaseSchema(stdClass $schema, Uri $uri)
71
    {
72 336
        $this->registerSchema($schema, $uri);
73 336
        $this->stack = [[$uri, $schema]];
74 336
    }
75
76
    /**
77
     * Clears internal schema resolution stack.
78
     */
79 299
    public function clearStack()
80
    {
81 299
        $this->stack = [];
82 299
    }
83
84
    /**
85
     * Returns the URI of the current schema.
86
     *
87
     * @return Uri
88
     *
89
     * @throws EmptyStackException
90
     */
91 61 View Code Duplication
    public function getCurrentUri()
92
    {
93 61
        if (count($this->stack) === 0) {
94 1
            throw new EmptyStackException();
95
        }
96
97 60
        return end($this->stack)[0];
98
    }
99
100
    /**
101
     * Returns the current schema.
102
     *
103
     * @return stdClass
104
     *
105
     * @throws EmptyStackException
106
     */
107 8 View Code Duplication
    public function getCurrentSchema()
108
    {
109 8
        if (count($this->stack) === 0) {
110 1
            throw new EmptyStackException();
111
        }
112
113 7
        return end($this->stack)[1];
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. If no schema is passed, the
131
     * current schema is reused (useful when entering a resolution scope
132
     * within the current schema).
133
     *
134
     * @param Uri      $uri
135
     * @param stdClass $schema
136
     *
137
     * @throws EmptyStackException
138
     */
139 23
    public function enter(Uri $uri, stdClass $schema = null)
140
    {
141 23
        $currentUri = $this->getCurrentUri();
142
143 23
        if (!$uri->isAbsolute()) {
144 3
            $uri->resolveAgainst($currentUri);
145 3
        }
146
147 23
        $this->stack[] = [$uri, $schema ?: $this->getCurrentSchema()];
148 23
    }
149
150
    /**
151
     * Removes the URI/schema pair at the top of the resolution stack,
152
     * thus returning to the previous URI/schema context.
153
     *
154
     * @throws EmptyStackException
155
     */
156 24
    public function leave()
157
    {
158 24
        if (count($this->stack) === 0) {
159 1
            throw new EmptyStackException();
160
        }
161
162 23
        array_pop($this->stack);
163 23
    }
164
165
    /**
166
     * Resolves a schema reference according to the JSON Reference
167
     * specification draft. Returns an array containing the resolved
168
     * URI and the resolved schema.
169
     *
170
     * @param stdClass $reference
171
     *
172
     * @throws InvalidPointerTargetException
173
     * @throws SelfReferencingPointerException
174
     *
175
     * @return array
176
     */
177 59
    public function resolve(stdClass $reference)
178
    {
179 59
        $baseUri = $this->getCurrentUri();
180 59
        $uri = new Uri($reference->{'$ref'});
181
182 59
        if (!$uri->isAbsolute()) {
183 46
            $uri->resolveAgainst($baseUri);
184 46
        }
185
186 59
        $identifier = $uri->getPrimaryResourceIdentifier();
187
188 59
        if (!isset($this->schemas[$identifier])) {
189 23
            $schema = $this->fetchSchemaAt($identifier);
190 16
            $this->registerSchema($schema, $uri);
191 16
        } else {
192 42
            $schema = $this->schemas[$identifier];
193
        }
194
195 52
        $resolved = $this->resolvePointer($schema, $uri);
196
197 43
        if ($resolved === $reference) {
198 2
            throw new SelfReferencingPointerException();
199
        }
200
201 41
        if (!is_object($resolved)) {
202 2
            throw new InvalidPointerTargetException([$uri->getRawUri()]);
203
        }
204
205 39
        return [$uri, $resolved];
206
    }
207
208
    /**
209
     * Caches a schema reference for future use.
210
     *
211
     * @param stdClass $schema
212
     * @param Uri      $uri
213
     */
214 336
    private function registerSchema(stdClass $schema, Uri $uri)
215
    {
216 336
        if (!isset($this->schemas[$uri->getPrimaryResourceIdentifier()])) {
217 336
            $this->schemas[$uri->getPrimaryResourceIdentifier()] = $schema;
218 336
        }
219 336
    }
220
221
    /**
222
     * Fetches a remote schema and ensures it is valid.
223
     *
224
     * @param string $uri
225
     *
226
     * @throws InvalidRemoteSchemaException
227
     * @throws JsonDecodeException
228
     *
229
     * @return stdClass
230
     */
231 23
    private function fetchSchemaAt($uri)
232
    {
233 23
        if ($hook = $this->preFetchHook) {
234 14
            $uri = $hook($uri);
235 14
        }
236
237 23
        set_error_handler(function ($severity, $error) use ($uri) {
238 5
            restore_error_handler();
239 5
            throw new UnfetchableUriException([$uri, $error, $severity]);
240 23
        });
241
242 23
        $content = file_get_contents($uri);
243 18
        restore_error_handler();
244
245 18
        $schema = json_decode($content);
246
247 18
        if (json_last_error() !== JSON_ERROR_NONE) {
248 1
            throw new JsonDecodeException(sprintf(
249 1
                'Cannot decode JSON from URI "%s" (error: %s)',
250 1
                $uri,
251 1
                Utils::lastJsonErrorMessage()
252 1
            ));
253
        }
254
255 17
        if (!is_object($schema)) {
256 1
            throw new InvalidRemoteSchemaException([$uri]);
257
        }
258
259 16
        return $schema;
260
    }
261
262
    /**
263
     * Resolves a JSON pointer according to RFC 6901.
264
     *
265
     * @param stdClass $schema
266
     * @param Uri      $pointerUri
267
     *
268
     * @return mixed
269
     *
270
     * @throws InvalidPointerIndexException
271
     * @throws InvalidSegmentTypeException
272
     * @throws UnresolvedPointerIndexException
273
     * @throws UnresolvedPointerPropertyException
274
     */
275 52
    private function resolvePointer(stdClass $schema, Uri $pointerUri)
276
    {
277 52
        $segments = $pointerUri->getPointerSegments();
278 52
        $pointer = $pointerUri->getRawPointer();
279 52
        $currentNode = $schema;
280
281 52
        for ($i = 0, $max = count($segments); $i < $max; ++$i) {
282 39
            if (is_object($currentNode)) {
283 39
                if (property_exists($currentNode, $segments[$i])) {
284 38
                    $currentNode = $currentNode->{$segments[$i]};
285 38
                    continue;
286
                }
287
288 3
                throw new UnresolvedPointerPropertyException([$segments[$i], $i, $pointer]);
289
            }
290
291 10
            if (is_array($currentNode)) {
292 10
                if (!preg_match('/^\d+$/', $segments[$i])) {
293 2
                    throw new InvalidPointerIndexException([$segments[$i], $i, $pointer]);
294
                }
295
296 10
                if (!isset($currentNode[$index = (int) $segments[$i]])) {
297 2
                    throw new UnresolvedPointerIndexException([$segments[$i], $i, $pointer]);
298
                }
299
300 8
                $currentNode = $currentNode[$index];
301 8
                continue;
302
            }
303
304 2
            throw new InvalidSegmentTypeException([$i, $pointer]);
305
        }
306
307 43
        return $currentNode;
308
    }
309
}
310