Completed
Push — master ( 5be3e8...abda03 )
by Matt
05:46 queued 05:41
created

Dereferencer::getReferences()   C

Complexity

Conditions 8
Paths 14

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 8.048

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 8
eloc 15
c 2
b 1
f 0
nc 14
nop 2
dl 0
loc 27
ccs 20
cts 22
cp 0.9091
crap 8.048
rs 5.3846
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 148
    public function __construct()
24
    {
25 148
        $this->registerFileLoader();
26 148
        $this->registerDefaultWebLoaders();
27 148
    }
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 148
    public function dereference($schema)
38
    {
39
        // If a string is provided, assume they passed a path.
40 148
        if (is_string($schema)) {
41 18
            $schema = $this->loadExternalRef($schema);
42 18
        }
43
44 148
        return $this->crawl($schema);
45
    }
46
47
    /**
48
     * Register a Loader for the given prefix.
49
     *
50
     * @param Loader $loader
51
     * @param string $prefix
52
     */
53 120
    public function registerLoader(Loader $loader, $prefix)
54
    {
55 120
        $this->loaders[$prefix] = $loader;
56 120
    }
57
58
    /**
59
     * Get the loader for the given prefix.
60
     *
61
     * @param $prefix
62
     *
63
     * @return Loader
64
     * @throws \InvalidArgumentException
65
     */
66 30
    private function getLoader($prefix)
67
    {
68 30
        if (!array_key_exists($prefix, $this->loaders)) {
69
            throw new \InvalidArgumentException(sprintf('A loader is not registered for the prefix "%s"', $prefix));
70
        }
71
72 30
        return $this->loaders[$prefix];
73
    }
74
75
    /**
76
     * Register the default file loader.
77
     */
78 148
    private function registerFileLoader()
79
    {
80 148
        $this->loaders['file'] = new FileLoader();
81 148
    }
82
83
    /**
84
     * Register the default web loaders.  If the curl extension is loaded,
85
     * the CurlWebLoader will be used.  Otherwise the FileGetContentsWebLoader
86
     * will be used.  You can override this by registering your own loader
87
     * for the 'http' and 'https' protocols.
88
     */
89 148
    private function registerDefaultWebLoaders()
90
    {
91 148
        if (function_exists('curl_init')) {
92 148
            $this->loaders['https'] = new CurlWebLoader('https://');
93 148
            $this->loaders['http']  = new CurlWebLoader('http://');
94 148
        } else {
95
            $this->loaders['https'] = new FileGetContentsWebLoader('https://');
96
            $this->loaders['http']  = new FileGetContentsWebLoader('http://');
97
        }
98 148
    }
99
100
    /**
101
     * Crawl the schema and resolve any references.
102
     *
103
     * @param object $schema
104
     *
105
     * @return object
106
     */
107 148
    private function crawl($schema)
108
    {
109 148
        $references = $this->getReferences($schema);
110
111 148
        foreach ($references as $path => $ref) {
112
            // resolve
113 38
            if ($this->isExternalRef($ref)) {
114 12
                $ref      = $this->makeReferenceAbsolute($schema, $path, $ref);
115 12
                $resolved = $this->loadExternalRef($ref);
116 12
                $resolved = $this->crawl($resolved);
117 12
            } else {
118 38
                $resolved = new Reference($schema, $ref);
119
            }
120
121
            // handle any fragments
122 38
            $fragment = parse_url($ref, PHP_URL_FRAGMENT);
123 38
            if ($this->isExternalRef($ref) && is_string($fragment)) {
124 4
                $pointer  = new Pointer($resolved);
125 4
                $resolved = $pointer->get($fragment);
126 4
            }
127
128
            // Immediately resolve any root references.
129 38
            if ($path === '') {
130 12
                while ($resolved instanceof Reference) {
131 8
                    $resolved = $resolved->resolve();
132 8
                }
133 12
            }
134
135
            // merge
136 38
            if ($path === '') {
137 12
                $this->mergeRootRef($schema, $resolved);
138 12
            } else {
139 38
                $pointer = new Pointer($schema);
140 38
                if ($pointer->has($path)) {
141 38
                    $pointer->set($path, $resolved);
142 38
                }
143
            }
144 148
        }
145
146 148
        return $schema;
147
    }
148
149
    /**
150
     * Recursively get all of the references for the given schema.
151
     * Returns an associative array like [path => reference].
152
     * Example:
153
     *
154
     * ['/properties' => '#/definitions/b']
155
     *
156
     * The path does NOT include the $ref.
157
     *
158
     * @param object $schema The schema to resolve references for.
159
     * @param string $path   The current schema path.
160
     *
161
     * @return array
162
     */
163 148
    private function getReferences($schema, $path = '')
164
    {
165 148
        $refs = [];
166
167 148
        if (!is_array($schema) && !is_object($schema)) {
168 40
            return $refs;
169
        }
170
171
        foreach ($schema as $attribute => $parameter) {
172 40
            if ($this->isRef($attribute, $parameter)) {
173
                $refs[$path] = $parameter;
174
            }
175 148
            if (is_object($parameter)) {
176 148
                $refs = array_merge($refs, $this->getReferences($parameter, $this->pathPush($path, $attribute)));
177 38
            }
178 38
            if (is_array($parameter)) {
179 148
                foreach ($parameter as $k => $v) {
180 84
                    $refs = array_merge(
181 84
                        $refs,
182 148
                        $this->getReferences($v, $this->pathPush($this->pathPush($path, $attribute), $k))
183 62
                    );
184 58
                }
185 58
            }
186 58
        }
187 58
188 62
        return $refs;
189 62
    }
190 148
191
    /**
192 148
     * Push a segment onto the given path.
193
     *
194
     * @param string $path
195
     * @param string $segment
196
     *
197
     * @return string
198
     */
199
    private function pathPush($path, $segment)
200
    {
201
        return $path . '/' . escape_pointer($segment);
202
    }
203 98
204
    /**
205 98
     * @param string $attribute
206
     * @param mixed $attributeValue
207
     *
208
     * @return bool
209
     */
210
    private function isRef($attribute, $attributeValue)
211
    {
212
        return $attribute === '$ref' && is_string($attributeValue);
213
    }
214 148
215
    /**
216 148
     * @param string $parameter
217
     *
218
     * @return bool
219
     */
220
    private function isInternalRef($parameter)
221
    {
222
        return is_string($parameter) && substr($parameter, 0, 1) === '#';
223
    }
224 38
225
    /**
226 38
     * @param string $parameter
227
     *
228
     * @return bool
229
     */
230
    private function isExternalRef($parameter)
231
    {
232
        return !$this->isInternalRef($parameter);
233
    }
234 38
235
    /**
236 38
     * Load an external ref and return the JSON object.
237
     *
238
     * @param string $reference
239
     *
240
     * @return object
241
     */
242
    private function loadExternalRef($reference)
243
    {
244
        $this->validateAbsolutePath($reference);
245
        list($prefix, $path) = explode('://', $reference, 2);
246 30
247
        $loader = $this->getLoader($prefix);
248 30
249 30
        $schema = $loader->load($path);
250
251 30
        return $schema;
252
    }
253 30
254
    /**
255 30
     * Merge a resolved reference into the root of the given schema.
256
     *
257
     * @param object $rootSchema
258
     * @param object $resolvedRef
259
     */
260
    private function mergeRootRef($rootSchema, $resolvedRef)
261
    {
262
        $ref = '$ref';
263
        unset($rootSchema->$ref);
264 12
        foreach (get_object_vars($resolvedRef) as $prop => $value) {
265
            $rootSchema->$prop = $value;
266 12
        }
267 12
    }
268 12
269 12
    /**
270 12
     * Validate an absolute path is valid.
271 12
     *
272
     * @param string $path
273
     */
274
    private function validateAbsolutePath($path)
275
    {
276
        if (!preg_match('#^.+\:\/\/.*#', $path)) {
277
            throw new \InvalidArgumentException(
278 30
                'Your path is missing a valid prefix.  The schema path should start with a prefix i.e. "file://".'
279
            );
280 30
        }
281
    }
282
283
    /**
284
     * Determine if a reference is relative.
285 30
     * A reference is relative if it does not being with a prefix.
286
     *
287
     * @param string $ref
288
     *
289
     * @return bool
290
     */
291
    private function isRelativeRef($ref)
292
    {
293
        return !preg_match('#^.+\:\/\/.*#', $ref);
294
    }
295 12
296
    /**
297 12
     * Take a relative reference, and prepend the id of the schema and any
298
     * sub schemas to get the absolute url.
299
     *
300
     * @param object $schema
301
     * @param string $path
302
     * @param string $ref
303
     *
304
     * @return string
305
     */
306
    private function makeReferenceAbsolute($schema, $path, $ref)
307
    {
308
        if (!$this->isRelativeRef($ref)) {
309
            return $ref;
310 12
        }
311
312 12
        $pointer     = new Pointer($schema);
313 12
        $baseUrl     = $pointer->get('/id');
314
        $currentPath = '';
315
        foreach (array_slice(explode('/', $path), 1) as $segment) {
316 4
            $currentPath .= '/' . $segment;
317 4
            if ($pointer->has($currentPath . '/id')) {
318 4
                $baseUrl .= $pointer->get($currentPath . '/id');
319 4
            }
320 4
        }
321 4
        $ref = $baseUrl . $ref;
322 4
323 4
        return $ref;
324 4
    }
325
}
326