Completed
Pull Request — master (#8)
by Jan
03:08
created

Resolver::leave()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2
Metric Value
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 9.4285
cc 2
eloc 4
nc 2
nop 0
crap 2
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 299
    public function hasBaseSchema()
60
    {
61 299
        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 current schema.
78
     *
79
     * @throws EmptyStackException
80
     */
81 299
    public function clearBaseSchema()
82
    {
83 299
        if (count($this->stack) === 0) {
84
            throw new EmptyStackException();
85
        }
86
87 299
        $this->stack = [];
88 299
    }
89
90
    /**
91
     * Returns the URI of the current schema.
92
     *
93
     * @return Uri
94
     *
95
     * @throws EmptyStackException
96
     */
97 61 View Code Duplication
    public function getCurrentUri()
98
    {
99 61
        if (count($this->stack) === 0) {
100 1
            throw new EmptyStackException();
101
        }
102
103 60
        return end($this->stack)[0];
104
    }
105
106
    /**
107
     * Returns the current schema.
108
     *
109
     * @return stdClass
110
     *
111
     * @throws EmptyStackException
112
     */
113 8 View Code Duplication
    public function getCurrentSchema()
114
    {
115 8
        if (count($this->stack) === 0) {
116 1
            throw new EmptyStackException();
117
        }
118
119 7
        return end($this->stack)[1];
120
    }
121
122
    /**
123
     * Sets an URI pre-fetch hook. The hook function will be called each time
124
     * a remote reference is about to be fetched. It is passed the original
125
     * pointer URI and must return a new URI string.
126
     *
127
     * @param Closure $preFetchHook
128
     */
129 300
    public function setPreFetchHook(Closure $preFetchHook)
130
    {
131 300
        $this->preFetchHook = $preFetchHook;
132 300
    }
133
134
    /**
135
     * Pushes an URI and its associated schema onto the resolution stack,
136
     * making them the current URI/schema pair. If no schema is passed, the
137
     * current schema is reused (useful when entering a resolution scope
138
     * within the current schema).
139
     *
140
     * @param Uri      $uri
141
     * @param stdClass $schema
142
     *
143
     * @throws EmptyStackException
144
     */
145 23
    public function enter(Uri $uri, stdClass $schema = null)
146
    {
147 23
        $currentUri = $this->getCurrentUri();
148
149 23
        if (!$uri->isAbsolute()) {
150 3
            $uri->resolveAgainst($currentUri);
151 3
        }
152
153 23
        $this->stack[] = [$uri, $schema ?: $this->getCurrentSchema()];
154 23
    }
155
156
    /**
157
     * Removes the URI/schema pair at the top of the resolution stack,
158
     * thus returning to the previous URI/schema context.
159
     *
160
     * @throws EmptyStackException
161
     */
162 24
    public function leave()
163
    {
164 24
        if (count($this->stack) === 0) {
165 1
            throw new EmptyStackException();
166
        }
167
168 23
        array_pop($this->stack);
169 23
    }
170
171
    /**
172
     * Resolves a schema reference according to the JSON Reference
173
     * specification draft. Returns an array containing the resolved
174
     * URI and the resolved schema.
175
     *
176
     * @param stdClass $reference
177
     *
178
     * @throws InvalidPointerTargetException
179
     * @throws SelfReferencingPointerException
180
     *
181
     * @return array
182
     */
183 59
    public function resolve(stdClass $reference)
184
    {
185 59
        $baseUri = $this->getCurrentUri();
186 59
        $uri = new Uri($reference->{'$ref'});
187
188 59
        if (!$uri->isAbsolute()) {
189 46
            $uri->resolveAgainst($baseUri);
190 46
        }
191
192 59
        $identifier = $uri->getPrimaryResourceIdentifier();
193
194 59
        if (!isset($this->schemas[$identifier])) {
195 23
            $schema = $this->fetchSchemaAt($identifier);
196 16
            $this->registerSchema($schema, $uri);
197 16
        } else {
198 42
            $schema = $this->schemas[$identifier];
199
        }
200
201 52
        $resolved = $this->resolvePointer($schema, $uri);
202
203 43
        if ($resolved === $reference) {
204 2
            throw new SelfReferencingPointerException();
205
        }
206
207 41
        if (!is_object($resolved)) {
208 2
            throw new InvalidPointerTargetException([$uri->getRawUri()]);
209
        }
210
211 39
        return [$uri, $resolved];
212
    }
213
214
    /**
215
     * Caches a schema reference for future use.
216
     *
217
     * @param stdClass $schema
218
     * @param Uri      $uri
219
     */
220 336
    private function registerSchema(stdClass $schema, Uri $uri)
221
    {
222 336
        if (!isset($this->schemas[$uri->getPrimaryResourceIdentifier()])) {
223 336
            $this->schemas[$uri->getPrimaryResourceIdentifier()] = $schema;
224 336
        }
225 336
    }
226
227
    /**
228
     * Fetches a remote schema and ensures it is valid.
229
     *
230
     * @param string $uri
231
     *
232
     * @throws InvalidRemoteSchemaException
233
     * @throws JsonDecodeException
234
     *
235
     * @return stdClass
236
     */
237 23
    private function fetchSchemaAt($uri)
238
    {
239 23
        if ($hook = $this->preFetchHook) {
240 14
            $uri = $hook($uri);
241 14
        }
242
243 23
        set_error_handler(function ($severity, $error) use ($uri) {
244 5
            restore_error_handler();
245 5
            throw new UnfetchableUriException([$uri, $error, $severity]);
246 23
        });
247
248 23
        $content = file_get_contents($uri);
249 18
        restore_error_handler();
250
251 18
        $schema = json_decode($content);
252
253 18
        if (json_last_error() !== JSON_ERROR_NONE) {
254 1
            throw new JsonDecodeException(sprintf(
255 1
                'Cannot decode JSON from URI "%s" (error: %s)',
256 1
                $uri,
257 1
                Utils::lastJsonErrorMessage()
258 1
            ));
259
        }
260
261 17
        if (!is_object($schema)) {
262 1
            throw new InvalidRemoteSchemaException([$uri]);
263
        }
264
265 16
        return $schema;
266
    }
267
268
    /**
269
     * Resolves a JSON pointer according to RFC 6901.
270
     *
271
     * @param stdClass $schema
272
     * @param Uri      $pointerUri
273
     *
274
     * @return mixed
275
     *
276
     * @throws InvalidPointerIndexException
277
     * @throws InvalidSegmentTypeException
278
     * @throws UnresolvedPointerIndexException
279
     * @throws UnresolvedPointerPropertyException
280
     */
281 52
    private function resolvePointer(stdClass $schema, Uri $pointerUri)
282
    {
283 52
        $segments = $pointerUri->getPointerSegments();
284 52
        $pointer = $pointerUri->getRawPointer();
285 52
        $currentNode = $schema;
286
287 52
        for ($i = 0, $max = count($segments); $i < $max; ++$i) {
288 39
            if (is_object($currentNode)) {
289 39
                if (property_exists($currentNode, $segments[$i])) {
290 38
                    $currentNode = $currentNode->{$segments[$i]};
291 38
                    continue;
292
                }
293
294 3
                throw new UnresolvedPointerPropertyException([$segments[$i], $i, $pointer]);
295
            }
296
297 10
            if (is_array($currentNode)) {
298 10
                if (!preg_match('/^\d+$/', $segments[$i])) {
299 2
                    throw new InvalidPointerIndexException([$segments[$i], $i, $pointer]);
300
                }
301
302 10
                if (!isset($currentNode[$index = (int) $segments[$i]])) {
303 2
                    throw new UnresolvedPointerIndexException([$segments[$i], $i, $pointer]);
304
                }
305
306 8
                $currentNode = $currentNode[$index];
307 8
                continue;
308
            }
309
310 2
            throw new InvalidSegmentTypeException([$i, $pointer]);
311
        }
312
313 43
        return $currentNode;
314
    }
315
}
316