Completed
Push — master ( 64ede7...feafc2 )
by Matt
02:35
created

Dereferencer::resolveReference()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 9
nc 2
nop 4
dl 0
loc 17
ccs 10
cts 10
cp 1
crap 2
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace League\JsonGuard;
4
5
use League\JsonGuard\Loaders\CurlWebLoader;
6
use League\JsonGuard\Loaders\FileGetContentsWebLoader;
7
use League\JsonGuard\Loaders\FileLoader;
8
9
/**
10
 * The Dereferencer resolves all external $refs and replaces
11
 * internal references with Reference objects.
12
 */
13
class Dereferencer
14
{
15
    /**
16
     * @var array
17
     */
18
    private $loaders;
19
20
    /**
21
     * Create a new Dereferencer.
22
     */
23 168
    public function __construct()
24
    {
25 168
        $this->registerFileLoader();
26 168
        $this->registerDefaultWebLoaders();
27 168
    }
28
29
    /**
30
     * Return the schema with all references resolved.
31
     *
32
     * @param string|object $schema Either a valid path like "http://json-schema.org/draft-03/schema#"
33
     *                              or the object resulting from a json_decode call.
34
     *
35
     * @return object
36
     */
37 166
    public function dereference($schema)
38
    {
39 166
        if (is_string($schema)) {
40 30
            $uri    = $schema;
41 30
            $schema = $this->loadExternalRef($uri);
42 28
            $schema = $this->resolveFragment($uri, $schema);
43
44 28
            return $this->crawl($schema, strip_fragment($uri));
45
        }
46
47 136
        return $this->crawl($schema);
48
    }
49
50
    /**
51
     * Register a Loader for the given prefix.
52
     *
53
     * @param Loader $loader
54
     * @param string $prefix
55
     */
56 122
    public function registerLoader(Loader $loader, $prefix)
57
    {
58 122
        $this->loaders[$prefix] = $loader;
59 122
    }
60
61
    /**
62
     * Get all registered loaders, keyed by the prefix they are registered to load schemas for.
63
     *
64
     * @return Loader[]
65
     */
66 2
    public function getLoaders()
67
    {
68 2
        return $this->loaders;
69
    }
70
71
    /**
72
     * Get the loader for the given prefix.
73
     *
74
     * @param string $prefix
75
     *
76
     * @return Loader
77
     * @throws \InvalidArgumentException
78
     */
79 46
    public function getLoader($prefix)
80
    {
81 46
        if (!array_key_exists($prefix, $this->loaders)) {
82 2
            throw new \InvalidArgumentException(sprintf('A loader is not registered for the prefix "%s"', $prefix));
83
        }
84
85 44
        return $this->loaders[$prefix];
86
    }
87
88
    /**
89
     * Register the default file loader.
90
     */
91 168
    private function registerFileLoader()
92
    {
93 168
        $this->loaders['file'] = new FileLoader();
94 168
    }
95
96
    /**
97
     * Register the default web loaders.  If the curl extension is loaded,
98
     * the CurlWebLoader will be used.  Otherwise the FileGetContentsWebLoader
99
     * will be used.  You can override this by registering your own loader
100
     * for the 'http' and 'https' protocols.
101
     */
102 168
    private function registerDefaultWebLoaders()
103
    {
104 168
        if (function_exists('curl_init')) {
105 168
            $this->loaders['https'] = new CurlWebLoader('https://');
106 168
            $this->loaders['http']  = new CurlWebLoader('http://');
107 168
        } else {
108
            $this->loaders['https'] = new FileGetContentsWebLoader('https://');
109
            $this->loaders['http']  = new FileGetContentsWebLoader('http://');
110
        }
111 168
    }
112
113
    /**
114
     * Crawl the schema and resolve any references.
115
     *
116
     * @param object      $schema
117
     * @param string|null $currentUri
118
     *
119
     * @return object
120
     */
121 164
    private function crawl($schema, $currentUri = null)
122
    {
123
        $references = schema_extract($schema, function ($keyword, $value) {
124 160
            return $this->isRef($keyword, $value);
125 164
        });
126
127 164
        foreach ($references as $path => $ref) {
128 54
            $this->resolveReference($schema, $path, $ref, $currentUri);
129 162
        }
130
131 162
        return $schema;
132
    }
133
134
    /**
135
     * @param object $schema
136
     * @param string $path
137
     * @param string $ref
138
     * @param string $currentUri
139
     */
140 54
    private function resolveReference($schema, $path, $ref, $currentUri)
141
    {
142
        // resolve
143 54
        if (!is_internal_ref($ref)) {
144 26
            $resolved = new Reference(function () use ($schema, $path, $ref, $currentUri) {
145 26
                return $this->resolveExternalReference($schema, $path, $ref, $currentUri);
146 26
            }, $ref);
147 26
        } else {
148 44
            $resolved = new Reference($schema, $ref);
149
        }
150
151
        // handle any fragments
152 54
        $resolved = $this->resolveFragment($ref, $resolved);
153
154
        // merge
155 54
        $this->mergeResolvedReference($schema, $resolved, $path);
156 52
    }
157
158
    /**
159
     * Resolve the external reference at the given path.
160
     *
161
     * @param  object      $schema     The JSON Schema
162
     * @param  string      $path       A JSON pointer to the $ref's location in the schema.
163
     * @param  string      $ref        The JSON reference
164
     * @param  string|null $currentUri The URI of the schema, or null if the schema was loaded from an object.
165
     *
166
     * @return object                  The schema with the reference resolved.
167
     */
168 26
    private function resolveExternalReference($schema, $path, $ref, $currentUri)
169
    {
170 26
        $ref      = $this->makeReferenceAbsolute($schema, $path, $ref, $currentUri);
171 26
        $resolved = $this->loadExternalRef($ref);
172
173 24
        return $this->crawl($resolved, strip_fragment($ref));
174
    }
175
176
    /**
177
     * Merge the resolved reference with the schema, at the given path.
178
     *
179
     * @param  object $schema   The schema to merge the resolved reference with
180
     * @param  object $resolved The resolved schema
181
     * @param  string $path     A JSON pointer to the path where the reference should be merged.
182
     *
183
     * @return void
184
     */
185 54
    private function mergeResolvedReference($schema, $resolved, $path)
186
    {
187 54
        if ($path === '') {
188
            // Immediately resolve any root references.
189 18
            while ($resolved instanceof Reference) {
190 18
                $resolved = $resolved->resolve();
191 16
            }
192 16
            $this->mergeRootRef($schema, $resolved);
193 16
        } else {
194 50
            $pointer = new Pointer($schema);
195 50
            if ($pointer->has($path)) {
196 50
                $pointer->set($path, $resolved);
197 50
            }
198
        }
199 52
    }
200
201
    /**
202
     * Check if the reference contains a fragment and resolve
203
     * the pointer.  Otherwise returns the original schema.
204
     *
205
     * @param  string $ref
206
     * @param  object $schema
207
     *
208
     * @return object
209
     */
210 56
    private function resolveFragment($ref, $schema)
211
    {
212 56
        $fragment = parse_url($ref, PHP_URL_FRAGMENT);
213 56
        if (!is_internal_ref($ref) && is_string($fragment)) {
214 10
            if ($schema instanceof Reference) {
215 6
                $schema = $schema->resolve();
216 6
            }
217 10
            $pointer  = new Pointer($schema);
218 10
            return $pointer->get($fragment);
219
        }
220
221 56
        return $schema;
222
    }
223
224
    /**
225
     * @param string $attribute
226
     * @param mixed  $attributeValue
227
     *
228
     * @return bool
229
     */
230 160
    private function isRef($attribute, $attributeValue)
231
    {
232 160
        return $attribute === '$ref' && is_string($attributeValue);
233
    }
234
235
    /**
236
     * Load an external ref and return the JSON object.
237
     *
238
     * @param string $reference
239
     *
240
     * @return object
241
     */
242 48
    private function loadExternalRef($reference)
243
    {
244 48
        $this->validateAbsolutePath($reference);
245 46
        list($prefix, $path) = explode('://', $reference, 2);
246 46
        $path = rtrim(strip_fragment($path), '#');
247
248 46
        $loader = $this->getLoader($prefix);
249
250 44
        $schema = $loader->load($path);
251
252 44
        return $schema;
253
    }
254
255
    /**
256
     * Merge a resolved reference into the root of the given schema.
257
     *
258
     * @param object $rootSchema
259
     * @param object $resolvedRef
260
     */
261 16
    private function mergeRootRef($rootSchema, $resolvedRef)
262
    {
263 16
        $ref = '$ref';
264 16
        unset($rootSchema->$ref);
265 16
        foreach (get_object_vars($resolvedRef) as $prop => $value) {
266 16
            $rootSchema->$prop = $value;
267 16
        }
268 16
    }
269
270
    /**
271
     * Validate an absolute path is valid.
272
     *
273
     * @param string $path
274
     */
275 48
    private function validateAbsolutePath($path)
276
    {
277 48
        if (!preg_match('#^.+\:\/\/.*#', $path)) {
278 2
            throw new \InvalidArgumentException(
279 2
                sprintf(
280
                    'Your path  "%s" is missing a valid prefix.  ' .
281 2
                    'The schema path should start with a prefix i.e. "file://".',
282
                    $path
283 2
                )
284 2
            );
285
        }
286 46
    }
287
288
    /**
289
     * Take a relative reference, and prepend the id of the schema and any
290
     * sub schemas to get the absolute url.
291
     *
292
     * @param object      $schema
293
     * @param string      $path
294
     * @param string      $ref
295
     * @param string|null $currentUri
296
     *
297
     * @return string
298
     */
299 26
    private function makeReferenceAbsolute($schema, $path, $ref, $currentUri = null)
300
    {
301
        // If the reference is absolute, we can just return it without walking the schema.
302 26
        if (!is_relative_ref($ref)) {
303 14
            return $ref;
304
        }
305
306 18
        $scope = $currentUri ?: '';
307 18
        $scope = $this->getResolvedResolutionScope($schema, $path, $scope);
308
309 18
        return resolve_uri($ref, $scope);
310
    }
311
312
    /**
313
     * Get the resolved resolution scope by walking the schema and resolving
314
     * every `id` against the most immediate parent scope.
315
     *
316
     * @see  http://json-schema.org/latest/json-schema-core.html#anchor27
317
     *
318
     * @param  object $schema
319
     * @param  string $path
320
     * @param  string $scope
321
     *
322
     * @return string
323
     */
324 18
    private function getResolvedResolutionScope($schema, $path, $scope)
325
    {
326 18
        $pointer     = new Pointer($schema);
327 18
        $currentPath = '';
328
329 18
        foreach (explode('/', $path) as $segment) {
330 18
            if (!empty($segment)) {
331 14
                $currentPath .= '/' . $segment;
332 14
            }
333 18
            if ($pointer->has($currentPath . '/id')) {
334 8
                $id = $pointer->get($currentPath . '/id');
335 8
                if (is_string($id)) {
336 8
                    $scope = resolve_uri($id, $scope);
337 8
                }
338 8
            }
339 18
        }
340
341 18
        return $scope;
342
    }
343
}
344