Skip to content

Commit fb736f7

Browse files
committed
WIP cache loaded files in reference context
simple cache implementation does not yet normalize relative paths in URIs
1 parent a27ffc4 commit fb736f7

3 files changed

Lines changed: 125 additions & 42 deletions

File tree

src/ReferenceContext.php

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77

88
namespace cebe\openapi;
99

10+
use cebe\openapi\exceptions\IOException;
1011
use cebe\openapi\exceptions\UnresolvableReferenceException;
12+
use cebe\openapi\json\JsonPointer;
13+
use cebe\openapi\spec\Reference;
14+
use Symfony\Component\Yaml\Yaml;
1115

1216
/**
1317
* ReferenceContext represents a context in which references are resolved.
@@ -27,17 +31,24 @@ class ReferenceContext
2731
* @var string
2832
*/
2933
private $_uri;
34+
/**
35+
* @var ReferenceContextCache
36+
*/
37+
private $_cache;
38+
3039

3140
/**
3241
* ReferenceContext constructor.
3342
* @param SpecObjectInterface $base the base object of the spec.
3443
* @param string $uri the URI to the base object.
44+
* @param ReferenceContextCache $cache cache instance for storing referenced file data.
3545
* @throws UnresolvableReferenceException in case an invalid or non-absolute URI is provided.
3646
*/
37-
public function __construct(?SpecObjectInterface $base, string $uri)
47+
public function __construct(?SpecObjectInterface $base, string $uri, $cache = null)
3848
{
3949
$this->_baseSpec = $base;
4050
$this->_uri = $this->normalizeUri($uri);
51+
$this->_cache = $cache ?? new ReferenceContextCache();
4152
}
4253

4354
/**
@@ -138,4 +149,68 @@ private function dirname($path)
138149
}
139150
return '';
140151
}
152+
153+
private $_fileCache;
154+
155+
/**
156+
* Fetch referenced file by URI.
157+
*
158+
* The current context will cache files by URI, so they are only loaded once.
159+
*
160+
* @throws IOException in case the file is not readable or fetching the file
161+
* from a remote URL failed.
162+
*/
163+
public function fetchReferencedFile($uri)
164+
{
165+
$content = file_get_contents($uri);
166+
if ($content === false) {
167+
$e = new IOException("Failed to read file: '$uri'");
168+
$e->fileName = $uri;
169+
throw $e;
170+
}
171+
// TODO lazy content detection, should be improved
172+
if (strpos(ltrim($content), '{') === 0) {
173+
return json_decode($content, true);
174+
} else {
175+
return Yaml::parse($content);
176+
}
177+
}
178+
179+
/**
180+
* Retrieve the referenced data via JSON pointer.
181+
*
182+
* This function caches referenced data to make sure references to the same
183+
* data structures end up being the same object instance in PHP.
184+
*
185+
* @param string $uri
186+
* @param JsonPointer $pointer
187+
* @param array $data
188+
* @param string|null $toType
189+
* @return SpecObjectInterface|array
190+
*/
191+
public function resolveReferenceData($uri, JsonPointer $pointer, $data, $toType)
192+
{
193+
$ref = $uri . '#' . $pointer->getPointer();
194+
if ($this->_cache->has($ref, $toType)) {
195+
return $this->_cache->get($ref, $toType);
196+
}
197+
198+
$referencedData = $pointer->evaluate($data);
199+
200+
if ($referencedData === null) {
201+
return null;
202+
}
203+
204+
// transitive reference
205+
if (isset($referencedData['$ref'])) {
206+
return (new Reference($referencedData, $toType))->resolve(new ReferenceContext(null, $uri));
207+
}
208+
/** @var SpecObjectInterface|array $referencedObject */
209+
$referencedObject = $toType !== null ? new $toType($referencedData) : $referencedData;
210+
211+
$this->_cache->set($ref, $toType, $referencedObject);
212+
213+
return $referencedObject;
214+
}
215+
141216
}

src/ReferenceContextCache.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (c) 2018 Carsten Brandt <mail@cebe.cc> and contributors
5+
* @license https://github.com/cebe/php-openapi/blob/master/LICENSE
6+
*/
7+
8+
namespace cebe\openapi;
9+
10+
/**
11+
* ReferenceContextCache represents a cache storage for caching content of referenced files.
12+
*/
13+
class ReferenceContextCache
14+
{
15+
private $_cache = [];
16+
17+
18+
public function set($ref, $type, $data)
19+
{
20+
$this->_cache[$ref][$type ?? ''] = $data;
21+
22+
// store fallback value for resolving with unknown type
23+
if ($type !== null && !isset($this->_cache[$ref][''])) {
24+
$this->_cache[$ref][''] = $data;
25+
}
26+
}
27+
28+
public function get($ref, $type)
29+
{
30+
return $this->_cache[$ref][$type ?? ''] ?? null;
31+
}
32+
33+
public function has($ref, $type)
34+
{
35+
return isset($this->_cache[$ref]) &&
36+
array_key_exists($type ?? '', $this->_cache[$ref]);
37+
}
38+
}

src/spec/Reference.php

Lines changed: 11 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -205,20 +205,19 @@ public function resolve(ReferenceContext $context = null)
205205

206206
// resolve in external document
207207
$file = $context->resolveRelativeUri($jsonReference->getDocumentUri());
208-
// TODO could be a good idea to cache loaded files in current context to avoid loading the same files over and over again
209-
$referencedDocument = $this->fetchReferencedFile($file);
210-
$referencedData = $jsonReference->getJsonPointer()->evaluate($referencedDocument);
211-
212-
if ($referencedData === null) {
213-
return null;
208+
try {
209+
$referencedDocument = $context->fetchReferencedFile($file);
210+
} catch (\Throwable $e) {
211+
$exception = new UnresolvableReferenceException(
212+
"Failed to resolve Reference '$this->_ref' to $this->_to Object: " . $e->getMessage(),
213+
$e->getCode(),
214+
$e
215+
);
216+
$exception->context = $this->getDocumentPosition();
217+
throw $exception;
214218
}
215219

216-
// transitive reference
217-
if (isset($referencedData['$ref'])) {
218-
return (new Reference($referencedData, $this->_to))->resolve(new ReferenceContext(null, $file));
219-
}
220-
/** @var SpecObjectInterface|array $referencedObject */
221-
$referencedObject = $this->_to !== null ? new $this->_to($referencedData) : $referencedData;
220+
$referencedObject = $context->resolveReferenceData($file, $jsonReference->getJsonPointer(), $referencedDocument, $this->_to);
222221

223222
if ($jsonReference->getJsonPointer()->getPointer() === '') {
224223
$newContext = new ReferenceContext($referencedObject instanceof SpecObjectInterface ? $referencedObject : null, $file);
@@ -258,35 +257,6 @@ public function resolve(ReferenceContext $context = null)
258257
}
259258
}
260259

261-
/**
262-
* @throws UnresolvableReferenceException
263-
*/
264-
private function fetchReferencedFile($uri)
265-
{
266-
try {
267-
$content = file_get_contents($uri);
268-
if ($content === false) {
269-
$e = new IOException("Failed to read file: '$uri'");
270-
$e->fileName = $uri;
271-
throw $e;
272-
}
273-
// TODO lazy content detection, should probably be improved
274-
if (strpos(ltrim($content), '{') === 0) {
275-
return json_decode($content, true);
276-
} else {
277-
return Yaml::parse($content);
278-
}
279-
} catch (\Throwable $e) {
280-
$exception = new UnresolvableReferenceException(
281-
"Failed to resolve Reference '$this->_ref' to $this->_to Object: " . $e->getMessage(),
282-
$e->getCode(),
283-
$e
284-
);
285-
$exception->context = $this->getDocumentPosition();
286-
throw $exception;
287-
}
288-
}
289-
290260
/**
291261
* Resolves all Reference Objects in this object and replaces them with their resolution.
292262
* @throws UnresolvableReferenceException

0 commit comments

Comments
 (0)