Resolver::resolvePointer()   C
last analyzed

Complexity

Conditions 7
Paths 7

Size

Total Lines 34
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 7

Importance

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