1 | <?php |
||
2 | |||
3 | namespace Swaggest\JsonSchema; |
||
4 | |||
5 | use PhpLang\ScopeExit; |
||
6 | use Swaggest\JsonDiff\JsonPointer; |
||
7 | use Swaggest\JsonSchema\Constraint\Ref; |
||
8 | use Swaggest\JsonSchema\RemoteRef\BasicFetcher; |
||
9 | |||
10 | class RefResolver |
||
11 | { |
||
12 | public $resolutionScope = ''; |
||
13 | public $url; |
||
14 | /** @var null|RefResolver */ |
||
15 | private $rootResolver; |
||
16 | |||
17 | /** |
||
18 | * @param mixed $resolutionScope |
||
19 | * @return string previous value |
||
20 | */ |
||
21 | 617 | public function setResolutionScope($resolutionScope) |
|
22 | { |
||
23 | 617 | $rootResolver = $this->rootResolver ? $this->rootResolver : $this; |
|
24 | 617 | if ($resolutionScope === $rootResolver->resolutionScope) { |
|
25 | 617 | return $resolutionScope; |
|
26 | } |
||
27 | 479 | $prev = $rootResolver->resolutionScope; |
|
28 | 479 | $rootResolver->resolutionScope = $resolutionScope; |
|
29 | 479 | return $prev; |
|
30 | } |
||
31 | |||
32 | /** |
||
33 | * @return string |
||
34 | */ |
||
35 | 616 | public function getResolutionScope() |
|
36 | { |
||
37 | 616 | $rootResolver = $this->rootResolver ? $this->rootResolver : $this; |
|
38 | 616 | return $rootResolver->resolutionScope; |
|
39 | } |
||
40 | |||
41 | |||
42 | /** |
||
43 | * @param string $id |
||
44 | * @return string |
||
45 | */ |
||
46 | 432 | public function updateResolutionScope($id) |
|
47 | { |
||
48 | 432 | $id = rtrim($id, '#'); // safe to trim because # in hashCode must be urlencoded to %23 |
|
49 | 432 | $rootResolver = $this->rootResolver ? $this->rootResolver : $this; |
|
50 | 432 | if ((strpos($id, '://') !== false) || 'urn:' === substr($id, 0, 4)) { |
|
51 | 432 | $prev = $rootResolver->setResolutionScope($id); |
|
52 | } else { |
||
53 | 380 | $id = Helper::resolveURI($rootResolver->resolutionScope, $id); |
|
54 | 380 | $prev = $rootResolver->setResolutionScope($id); |
|
55 | } |
||
56 | |||
57 | 432 | return $prev; |
|
58 | } |
||
59 | |||
60 | 432 | public function setupResolutionScope($id, $data) |
|
61 | { |
||
62 | 432 | $rootResolver = $this->rootResolver ? $this->rootResolver : $this; |
|
63 | |||
64 | 432 | $prev = $rootResolver->updateResolutionScope($id); |
|
65 | |||
66 | 432 | $refParts = explode('#', $rootResolver->resolutionScope, 2); |
|
67 | |||
68 | 432 | if ($refParts[0]) { // external uri |
|
69 | 432 | $resolver = &$rootResolver->remoteRefResolvers[$refParts[0]]; |
|
70 | 432 | if ($resolver === null) { |
|
71 | 423 | $resolver = new RefResolver(); |
|
72 | 423 | $resolver->rootResolver = $rootResolver; |
|
73 | 423 | $resolver->url = $refParts[0]; |
|
74 | 432 | $this->remoteRefResolvers[$refParts[0]] = $resolver; |
|
75 | } |
||
76 | } else { // local uri |
||
77 | 6 | $resolver = $this; |
|
78 | } |
||
79 | |||
80 | 432 | if (empty($refParts[1])) { |
|
81 | 432 | $resolver->rootData = $data; |
|
82 | } else { |
||
83 | 14 | $refPath = '#' . $refParts[1]; |
|
84 | 14 | $resolver->refs[$refPath] = new Ref($refPath, $data); |
|
85 | } |
||
86 | |||
87 | 432 | return $prev; |
|
88 | } |
||
89 | |||
90 | private $rootData; |
||
91 | |||
92 | /** @var Ref[] */ |
||
93 | private $refs = array(); |
||
94 | |||
95 | /** @var RefResolver[]|null[] */ |
||
96 | private $remoteRefResolvers = array(); |
||
97 | |||
98 | /** @var RemoteRefProvider */ |
||
99 | private $refProvider; |
||
100 | |||
101 | /** |
||
102 | * RefResolver constructor. |
||
103 | * @param JsonSchema $rootData |
||
104 | */ |
||
105 | 3271 | public function __construct($rootData = null) |
|
106 | { |
||
107 | 3271 | $this->rootData = $rootData; |
|
108 | 3271 | } |
|
109 | |||
110 | 1766 | public function setRootData($rootData) |
|
111 | { |
||
112 | 1766 | $this->rootData = $rootData; |
|
113 | 1766 | return $this; |
|
114 | } |
||
115 | |||
116 | |||
117 | 3193 | public function setRemoteRefProvider(RemoteRefProvider $provider) |
|
118 | { |
||
119 | 3193 | $this->refProvider = $provider; |
|
120 | 3193 | return $this; |
|
121 | } |
||
122 | |||
123 | 130 | private function getRefProvider() |
|
124 | { |
||
125 | 130 | if (null === $this->refProvider) { |
|
126 | 7 | $this->refProvider = new BasicFetcher(); |
|
127 | } |
||
128 | 130 | return $this->refProvider; |
|
129 | } |
||
130 | |||
131 | /** |
||
132 | * @param string $referencePath |
||
133 | * @return Ref |
||
134 | * @throws Exception |
||
135 | */ |
||
136 | 414 | public function resolveReference($referencePath) |
|
137 | { |
||
138 | 414 | if ($this->resolutionScope) { |
|
139 | 188 | $referencePath = Helper::resolveURI($this->resolutionScope, $referencePath); |
|
140 | } |
||
141 | |||
142 | 414 | $refParts = explode('#', $referencePath, 2); |
|
143 | 414 | $url = rtrim($refParts[0], '#'); |
|
144 | 414 | $refLocalPath = isset($refParts[1]) ? '#' . $refParts[1] : '#'; |
|
145 | |||
146 | 414 | if ($url === $this->url) { |
|
147 | $referencePath = $refLocalPath; |
||
148 | } |
||
149 | |||
150 | /** @var null|Ref $ref */ |
||
151 | 414 | $ref = &$this->refs[$referencePath]; |
|
152 | |||
153 | 414 | $refResolver = $this; |
|
154 | |||
155 | 414 | if (null === $ref) { |
|
156 | 408 | if ($referencePath[0] === '#') { |
|
157 | 402 | if ($referencePath === '#') { |
|
158 | 159 | $ref = new Ref($referencePath, $refResolver->rootData); |
|
159 | } else { |
||
160 | 320 | $ref = new Ref($referencePath); |
|
161 | try { |
||
162 | 320 | $path = JsonPointer::splitPath($referencePath); |
|
163 | } catch (\Swaggest\JsonDiff\Exception $e) { |
||
164 | throw new InvalidValue('Invalid reference: ' . $referencePath . ', ' . $e->getMessage()); |
||
165 | } |
||
166 | |||
167 | /** @var JsonSchema|\stdClass $branch */ |
||
168 | 320 | $branch = &$refResolver->rootData; |
|
169 | 320 | while (!empty($path)) { |
|
170 | 320 | if (isset($branch->{Schema::PROP_ID_D4}) && is_string($branch->{Schema::PROP_ID_D4})) { |
|
171 | 27 | $refResolver->updateResolutionScope($branch->{Schema::PROP_ID_D4}); |
|
172 | } |
||
173 | 320 | if (isset($branch->{Schema::PROP_ID}) && is_string($branch->{Schema::PROP_ID})) { |
|
174 | 77 | $refResolver->updateResolutionScope($branch->{Schema::PROP_ID}); |
|
175 | } |
||
176 | |||
177 | 320 | $folder = array_shift($path); |
|
178 | |||
179 | 320 | if ($branch instanceof \stdClass && isset($branch->$folder)) { |
|
180 | 320 | $branch = &$branch->$folder; |
|
181 | 12 | } elseif (is_array($branch) && isset($branch[$folder])) { |
|
182 | 12 | $branch = &$branch[$folder]; |
|
183 | } else { |
||
184 | throw new InvalidValue('Could not resolve ' . $referencePath . '@' . $this->getResolutionScope() . ': ' . $folder); |
||
185 | } |
||
186 | } |
||
187 | 402 | $ref->setData($branch); |
|
188 | } |
||
189 | } else { |
||
190 | 228 | if ($url !== $this->url) { |
|
191 | 228 | $rootResolver = $this->rootResolver ? $this->rootResolver : $this; |
|
192 | /** @var null|RefResolver $refResolver */ |
||
193 | 228 | $refResolver = &$rootResolver->remoteRefResolvers[$url]; |
|
194 | 228 | $this->setResolutionScope($url); |
|
195 | 228 | if (null === $refResolver) { |
|
196 | 130 | $rootData = $rootResolver->getRefProvider()->getSchemaData($url); |
|
197 | 130 | if ($rootData === null || $rootData === false) { |
|
198 | 130 | throw new Exception("Failed to decode content from $url", Exception::RESOLVE_FAILED); |
|
199 | 130 | } |
|
200 | 130 | ||
201 | 130 | $refResolver = new RefResolver($rootData); |
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
202 | 130 | $refResolver->rootResolver = $rootResolver; |
|
203 | 130 | $refResolver->refProvider = $this->refProvider; |
|
204 | $refResolver->url = $url; |
||
205 | $rootResolver->setResolutionScope($url); |
||
206 | $options = new Context(); // todo pass real ctx here, v0.13.0 |
||
207 | 228 | $rootResolver->preProcessReferences($rootData, $options); |
|
208 | } |
||
209 | } |
||
210 | |||
211 | 414 | $ref = $refResolver->resolveReference($refLocalPath); |
|
212 | } |
||
213 | } |
||
214 | |||
215 | return $ref; |
||
216 | } |
||
217 | |||
218 | |||
219 | /** |
||
220 | * @param mixed $data |
||
221 | 3271 | * @param Context $options |
|
222 | * @param int $nestingLevel |
||
223 | 3271 | * @throws Exception |
|
224 | */ |
||
225 | public function preProcessReferences($data, Context $options, $nestingLevel = 0) |
||
226 | 3271 | { |
|
227 | 1401 | if ($nestingLevel > 200) { |
|
228 | 1256 | throw new Exception('Too deep nesting level', Exception::DEEP_NESTING); |
|
229 | } |
||
230 | 3269 | if (is_array($data)) { |
|
231 | foreach ($data as $key => $item) { |
||
232 | $this->preProcessReferences($item, $options, $nestingLevel + 1); |
||
233 | 3253 | } |
|
234 | 3253 | } elseif ($data instanceof \stdClass) { |
|
235 | 3253 | /** @var JsonSchema $data */ |
|
236 | if ( |
||
237 | 384 | isset($data->{Schema::PROP_ID_D4}) |
|
238 | && is_string($data->{Schema::PROP_ID_D4}) |
||
239 | && (($options->version === Schema::VERSION_AUTO) || $options->version === Schema::VERSION_DRAFT_04) |
||
240 | 384 | ) { |
|
241 | 384 | $prev = $this->setupResolutionScope($data->{Schema::PROP_ID_D4}, $data); |
|
242 | /** @noinspection PhpUnusedLocalVariableInspection */ |
||
243 | $_ = new ScopeExit(function () use ($prev) { |
||
0 ignored issues
–
show
|
|||
244 | 3253 | $this->setResolutionScope($prev); |
|
245 | 3253 | }); |
|
246 | 3253 | } |
|
247 | |||
248 | 381 | if (isset($data->{Schema::PROP_ID}) |
|
249 | && is_string($data->{Schema::PROP_ID}) |
||
250 | 381 | && (($options->version === Schema::VERSION_AUTO) || $options->version >= Schema::VERSION_DRAFT_06) |
|
251 | 381 | ) { |
|
252 | 381 | $prev = $this->setupResolutionScope($data->{Schema::PROP_ID}, $data); |
|
253 | /** @noinspection PhpUnusedLocalVariableInspection */ |
||
254 | $_ = new ScopeExit(function () use ($prev) { |
||
255 | $this->setResolutionScope($prev); |
||
256 | 3253 | }); |
|
257 | 3251 | } |
|
258 | |||
259 | |||
260 | 3271 | foreach ((array)$data as $key => $value) { |
|
261 | $this->preProcessReferences($value, $options, $nestingLevel + 1); |
||
262 | } |
||
263 | } |
||
264 | } |
||
265 | |||
266 | |||
267 | } |