Completed
Push — master ( 0f5097...9c8f19 )
by Viacheslav
12:56 queued 02:59
created

src/RefResolver.php (2 issues)

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
It seems like $rootData can also be of type stdClass; however, parameter $rootData of Swaggest\JsonSchema\RefResolver::__construct() does only seem to accept Swaggest\JsonSchema\JsonSchema, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

201
                        $refResolver = new RefResolver(/** @scrutinizer ignore-type */ $rootData);
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
The assignment to $_ is dead and can be removed.
Loading history...
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
}