 swaggest    /
                    php-json-schema
                      swaggest    /
                    php-json-schema
                
                            | 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 InvalidRef('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 InvalidRef('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 | } | 
