Completed
Push — master ( 78f34b...2a7e47 )
by Matt
10s
created

Dereferencer::loadExternalRef()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 1
dl 0
loc 8
ccs 5
cts 5
cp 1
crap 1
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
        list($prefix, $path) = parse_external_ref($reference);
187 54
        $loader = $this->loaderManager->getLoader($prefix);
188
        $schema = $loader->load($path);
189 18
190 18
        return $schema;
191 16
    }
192 16
193 16
    /**
194 50
     * Merge a resolved reference into the root of the given schema.
195 50
     *
196 50
     * @param object $rootSchema
197 50
     * @param object $resolvedRef
198
     */
199 52
    private function mergeRootRef($rootSchema, $resolvedRef)
200
    {
201
        $ref = '$ref';
202
        unset($rootSchema->$ref);
203
        foreach (get_object_vars($resolvedRef) as $prop => $value) {
204
            $rootSchema->$prop = $value;
205
        }
206
    }
207
208
    /**
209
     * Take a relative reference, and prepend the id of the schema and any
210 56
     * sub schemas to get the absolute url.
211
     *
212 56
     * @param object      $schema
213 56
     * @param string      $path
214 10
     * @param string      $ref
215 6
     * @param string|null $currentUri
216 6
     *
217 10
     * @return string
218 10
     */
219
    private function makeReferenceAbsolute($schema, $path, $ref, $currentUri = null)
220
    {
221 56
        // If the reference is absolute, we can just return it without walking the schema.
222
        if (!is_relative_ref($ref)) {
223
            return $ref;
224
        }
225
226
        $scope = $currentUri ?: '';
227
        $scope = $this->getResolvedResolutionScope($schema, $path, $scope);
228
229
        return resolve_uri($ref, $scope);
230 160
    }
231
232 160
    /**
233
     * Get the resolved resolution scope by walking the schema and resolving
234
     * every `id` against the most immediate parent scope.
235
     *
236
     * @see  http://json-schema.org/latest/json-schema-core.html#anchor27
237
     *
238
     * @param  object $schema
239
     * @param  string $path
240
     * @param  string $scope
241
     *
242 48
     * @return string
243
     */
244 48
    private function getResolvedResolutionScope($schema, $path, $scope)
245 46
    {
246 46
        $pointer     = new Pointer($schema);
247
        $currentPath = '';
248 46
249
        foreach (explode('/', $path) as $segment) {
250 44
            if (!empty($segment)) {
251
                $currentPath .= '/' . $segment;
252 44
            }
253
            if ($pointer->has($currentPath . '/id')) {
254
                $id = $pointer->get($currentPath . '/id');
255
                if (is_string($id)) {
256
                    $scope = resolve_uri($id, $scope);
257
                }
258
            }
259
        }
260
261 16
        return $scope;
262
    }
263
}
264