Completed
Pull Request — master (#15)
by Matt
02:49
created

Dereferencer::isRef()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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