@@ -22,7 +22,7 @@ class SchemaDefinitionDictionary extends ArrayObject
2222 /**
2323 * SchemaDefinitionDictionary constructor.
2424 */
25- public function __construct (private string $ sourceDirectory )
25+ public function __construct (private ? JsonSchema $ schema = null )
2626 {
2727 parent ::__construct ();
2828 }
@@ -129,18 +129,16 @@ public function getDefinition(string $key, SchemaProcessor $schemaProcessor, arr
129129 /**
130130 * @throws SchemaException
131131 */
132- protected function parseExternalFile (
132+ private function parseExternalFile (
133133 string $ jsonSchemaFile ,
134134 string $ externalKey ,
135135 SchemaProcessor $ schemaProcessor ,
136136 array &$ path ,
137137 ): ?SchemaDefinition {
138- $ jsonSchemaFilePath = filter_var ($ jsonSchemaFile , FILTER_VALIDATE_URL )
139- ? $ jsonSchemaFile
140- : $ this ->sourceDirectory . '/ ' . $ jsonSchemaFile ;
138+ $ jsonSchemaFilePath = $ this ->getFullRefURL ($ jsonSchemaFile ) ?: $ this ->getLocalRefPath ($ jsonSchemaFile );
141139
142- if (! filter_var ( $ jsonSchemaFilePath, FILTER_VALIDATE_URL ) && ! is_file ( $ jsonSchemaFilePath ) ) {
143- throw new SchemaException ("Reference to non existing JSON-Schema file $ jsonSchemaFilePath " );
140+ if ($ jsonSchemaFilePath === null ) {
141+ throw new SchemaException ("Reference to non existing JSON-Schema file $ jsonSchemaFile " );
144142 }
145143
146144 $ jsonSchema = file_get_contents ($ jsonSchemaFilePath );
@@ -154,13 +152,105 @@ protected function parseExternalFile(
154152 '' ,
155153 $ schemaProcessor ->getCurrentClassPath (),
156154 'ExternalSchema ' ,
157- new JsonSchema ($ jsonSchemaFilePath , $ decodedJsonSchema ),
158- new self (dirname ( $ jsonSchemaFilePath ) ),
155+ $ externalSchema = new JsonSchema ($ jsonSchemaFilePath , $ decodedJsonSchema ),
156+ new self ($ externalSchema ),
159157 );
160158
161159 $ schema ->getSchemaDictionary ()->setUpDefinitionDictionary ($ schemaProcessor , $ schema );
162160 $ this ->parsedExternalFileSchemas [$ jsonSchemaFile ] = $ schema ;
163161
164162 return $ schema ->getSchemaDictionary ()->getDefinition ($ externalKey , $ schemaProcessor , $ path );
165163 }
164+
165+ /**
166+ * Try to build a full URL to fetch the schema from utilizing the $id field of the schema
167+ */
168+ private function getFullRefURL (string $ jsonSchemaFile ): ?string
169+ {
170+ if (filter_var ($ jsonSchemaFile , FILTER_VALIDATE_URL )) {
171+ return $ jsonSchemaFile ;
172+ }
173+
174+ if ($ this ->schema === null
175+ || !filter_var ($ this ->schema ->getJson ()['$id ' ] ?? $ this ->schema ->getFile (), FILTER_VALIDATE_URL )
176+ || ($ idURL = parse_url ($ this ->schema ->getJson ()['$id ' ] ?? $ this ->schema ->getFile ())) === false
177+ ) {
178+ return null ;
179+ }
180+
181+ $ baseURL = $ idURL ['scheme ' ] . ':// ' . $ idURL ['host ' ] . (isset ($ idURL ['port ' ]) ? ': ' . $ idURL ['port ' ] : '' );
182+
183+ // root relative $ref
184+ if (str_starts_with ($ jsonSchemaFile , '/ ' )) {
185+ return $ baseURL . $ jsonSchemaFile ;
186+ }
187+
188+ // relative $ref against the path of $id
189+ $ segments = explode ('/ ' , rtrim (dirname ($ idURL ['path ' ] ?? '/ ' ), '/ ' ) . '/ ' . $ jsonSchemaFile );
190+ $ output = [];
191+
192+ foreach ($ segments as $ seg ) {
193+ if ($ seg === '' || $ seg === '. ' ) {
194+ continue ;
195+ }
196+ if ($ seg === '.. ' ) {
197+ array_pop ($ output );
198+ continue ;
199+ }
200+ $ output [] = $ seg ;
201+ }
202+
203+ return $ baseURL . '/ ' . implode ('/ ' , $ output );
204+ }
205+
206+ private function getLocalRefPath (string $ jsonSchemaFile ): ?string
207+ {
208+ $ currentDir = dirname ($ this ->schema ->getFile ());
209+ // windows compatibility
210+ $ jsonSchemaFile = str_replace ('\\' , '/ ' , $ jsonSchemaFile );
211+
212+ // relative paths to the current location
213+ if (!str_starts_with ($ jsonSchemaFile , '/ ' )) {
214+ $ candidate = $ this ->normalizePath ($ currentDir . '/ ' . $ jsonSchemaFile );
215+ return file_exists ($ candidate ) ? $ candidate : null ;
216+ }
217+
218+ // absolute paths: traverse up to find the context root directory
219+ $ relative = ltrim ($ jsonSchemaFile , '/ ' );
220+
221+ $ dir = $ currentDir ;
222+ while (true ) {
223+ $ candidate = $ this ->normalizePath ($ dir . '/ ' . $ relative );
224+ if (file_exists ($ candidate )) {
225+ return $ candidate ;
226+ }
227+
228+ $ parent = dirname ($ dir );
229+ if ($ parent === $ dir ) {
230+ break ;
231+ }
232+ $ dir = $ parent ;
233+ }
234+
235+ return null ;
236+ }
237+
238+ private function normalizePath (string $ path ): string
239+ {
240+ $ segments = explode ('/ ' , str_replace ('\\' , '/ ' , $ path ));
241+ $ output = [];
242+
243+ foreach ($ segments as $ seg ) {
244+ if ($ seg === '' || $ seg === '. ' ) {
245+ continue ;
246+ }
247+ if ($ seg === '.. ' ) {
248+ array_pop ($ output );
249+ continue ;
250+ }
251+ $ output [] = $ seg ;
252+ }
253+
254+ return str_replace ('/ ' , DIRECTORY_SEPARATOR , implode ('/ ' , $ output ));
255+ }
166256}
0 commit comments