Completed
Pull Request — master (#91)
by Matt
13:39
created

Dereferencer::getLoader()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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