Completed
Pull Request — master (#92)
by Matt
10:21
created

Dereferencer   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 255
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
dl 0
loc 255
ccs 89
cts 89
cp 1
rs 9.8
c 4
b 0
f 0
wmc 31
lcom 1
cbo 4

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 2
A dereference() 0 12 2
A getLoaderManager() 0 4 1
A crawl() 0 12 2
A resolveReference() 0 17 2
A resolveExternalReference() 0 7 1
A mergeResolvedReference() 0 15 4
A resolveFragment() 0 13 4
A isRef() 0 4 2
A loadExternalRef() 0 8 1
A mergeRootRef() 0 8 2
A makeReferenceAbsolute() 0 12 3
B getResolvedResolutionScope() 0 19 5
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 164
    public function __construct(LoaderManager $loaderManager = null)
22
    {
23 164
        $this->loaderManager = $loaderManager ?: new LoaderManager();
24 164
    }
25
26
    /**
27
     * 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 164
    public function dereference($schema)
35
    {
36 164
        if (is_string($schema)) {
37 28
            $uri    = $schema;
38 28
            $schema = $this->loadExternalRef($uri);
39 28
            $schema = $this->resolveFragment($uri, $schema);
40
41 28
            return $this->crawl($schema, strip_fragment($uri));
42
        }
43
44 136
        return $this->crawl($schema);
45
    }
46
47
    /**
48
     * @return LoaderManager
49
     */
50 122
    public function getLoaderManager()
51
    {
52 122
        return $this->loaderManager;
53
    }
54
55
    /**
56
     * Crawl the schema and resolve any references.
57
     *
58
     * @param object      $schema
59
     * @param string|null $currentUri
60
     *
61
     * @return object
62
     */
63 164
    private function crawl($schema, $currentUri = null)
64
    {
65
        $references = schema_extract($schema, function ($keyword, $value) {
66 160
            return $this->isRef($keyword, $value);
67 164
        });
68
69 164
        foreach ($references as $path => $ref) {
70 54
            $this->resolveReference($schema, $path, $ref, $currentUri);
71 162
        }
72
73 162
        return $schema;
74
    }
75
76
    /**
77
     * @param object $schema
78
     * @param string $path
79
     * @param string $ref
80
     * @param string $currentUri
81
     */
82 54
    private function resolveReference($schema, $path, $ref, $currentUri)
83
    {
84
        // resolve
85 54
        if (!is_internal_ref($ref)) {
86 26
            $resolved = new Reference(function () use ($schema, $path, $ref, $currentUri) {
87 26
                return $this->resolveExternalReference($schema, $path, $ref, $currentUri);
88 26
            }, $ref);
89 26
        } else {
90 44
            $resolved = new Reference($schema, $ref);
91
        }
92
93
        // handle any fragments
94 54
        $resolved = $this->resolveFragment($ref, $resolved);
95
96
        // merge
97 54
        $this->mergeResolvedReference($schema, $resolved, $path);
98 52
    }
99
100
    /**
101
     * Resolve the external reference at the given path.
102
     *
103
     * @param  object      $schema     The JSON Schema
104
     * @param  string      $path       A JSON pointer to the $ref's location in the schema.
105
     * @param  string      $ref        The JSON reference
106
     * @param  string|null $currentUri The URI of the schema, or null if the schema was loaded from an object.
107
     *
108
     * @return object                  The schema with the reference resolved.
109
     */
110 26
    private function resolveExternalReference($schema, $path, $ref, $currentUri)
111
    {
112 26
        $ref      = $this->makeReferenceAbsolute($schema, $path, $ref, $currentUri);
113 26
        $resolved = $this->loadExternalRef($ref);
114
115 24
        return $this->crawl($resolved, strip_fragment($ref));
116
    }
117
118
    /**
119
     * Merge the resolved reference with the schema, at the given path.
120
     *
121
     * @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
     *
125
     * @return void
126
     */
127 54
    private function mergeResolvedReference($schema, $resolved, $path)
128
    {
129 54
        if ($path === '') {
130
            // Immediately resolve any root references.
131 18
            while ($resolved instanceof Reference) {
132 18
                $resolved = $resolved->resolve();
133 16
            }
134 16
            $this->mergeRootRef($schema, $resolved);
135 16
        } else {
136 50
            $pointer = new Pointer($schema);
137 50
            if ($pointer->has($path)) {
138 50
                $pointer->set($path, $resolved);
139 50
            }
140
        }
141 52
    }
142
143
    /**
144
     * Check if the reference contains a fragment and resolve
145
     * the pointer.  Otherwise returns the original schema.
146
     *
147
     * @param  string $ref
148
     * @param  object $schema
149
     *
150
     * @return object
151
     */
152 56
    private function resolveFragment($ref, $schema)
153
    {
154 56
        $fragment = parse_url($ref, PHP_URL_FRAGMENT);
155 56
        if (!is_internal_ref($ref) && is_string($fragment)) {
156 10
            if ($schema instanceof Reference) {
157 6
                $schema = $schema->resolve();
158 6
            }
159 10
            $pointer  = new Pointer($schema);
160 10
            return $pointer->get($fragment);
161
        }
162
163 56
        return $schema;
164
    }
165
166
    /**
167
     * @param string $attribute
168
     * @param mixed  $attributeValue
169
     *
170
     * @return bool
171
     */
172 160
    private function isRef($attribute, $attributeValue)
173
    {
174 160
        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 46
    private function loadExternalRef($reference)
185
    {
186 46
        list($prefix, $path) = parse_external_ref($reference);
187 44
        $loader = $this->loaderManager->getLoader($prefix);
188 44
        $schema = $loader->load($path);
189
190 44
        return $schema;
191
    }
192
193
    /**
194
     * Merge a resolved reference into the root of the given schema.
195
     *
196
     * @param object $rootSchema
197
     * @param object $resolvedRef
198
     */
199 16
    private function mergeRootRef($rootSchema, $resolvedRef)
200
    {
201 16
        $ref = '$ref';
202 16
        unset($rootSchema->$ref);
203 16
        foreach (get_object_vars($resolvedRef) as $prop => $value) {
204 16
            $rootSchema->$prop = $value;
205 16
        }
206 16
    }
207
208
    /**
209
     * Take a relative reference, and prepend the id of the schema and any
210
     * sub schemas to get the absolute url.
211
     *
212
     * @param object      $schema
213
     * @param string      $path
214
     * @param string      $ref
215
     * @param string|null $currentUri
216
     *
217
     * @return string
218
     */
219 26
    private function makeReferenceAbsolute($schema, $path, $ref, $currentUri = null)
220
    {
221
        // If the reference is absolute, we can just return it without walking the schema.
222 26
        if (!is_relative_ref($ref)) {
223 14
            return $ref;
224
        }
225
226 18
        $scope = $currentUri ?: '';
227 18
        $scope = $this->getResolvedResolutionScope($schema, $path, $scope);
228
229 18
        return resolve_uri($ref, $scope);
230
    }
231
232
    /**
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
     * @return string
243
     */
244 18
    private function getResolvedResolutionScope($schema, $path, $scope)
245
    {
246 18
        $pointer     = new Pointer($schema);
247 18
        $currentPath = '';
248
249 18
        foreach (explode('/', $path) as $segment) {
250 18
            if (!empty($segment)) {
251 14
                $currentPath .= '/' . $segment;
252 14
            }
253 18
            if ($pointer->has($currentPath . '/id')) {
254 8
                $id = $pointer->get($currentPath . '/id');
255 8
                if (is_string($id)) {
256 8
                    $scope = resolve_uri($id, $scope);
257 8
                }
258 8
            }
259 18
        }
260
261 18
        return $scope;
262
    }
263
}
264