Compare commits
13
Commits
6193b450ac
...
1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fde9661c2c | ||
|
|
6f2fd40464 | ||
|
|
bcfeedc703 | ||
|
|
c4f5504fdf | ||
|
|
55419acb09 | ||
|
|
1f938cf19d | ||
|
|
29e59c2b3c | ||
|
|
d3741e03ba | ||
|
|
b3c810473f | ||
|
|
b9141326b1 | ||
|
|
60a6db97bc | ||
|
|
0295627da1 | ||
|
|
bb4a95e8de |
No files matched your search
@@ -0,0 +1 @@
|
||||
Testing: Run `sh run-docker-testsuite.sh` instead of trying to run phpUnit directly.
|
||||
@@ -36,7 +36,11 @@ class COIDParser {
|
||||
(strtolower(substr($coidString, 0, 7)) == 'coid://') ? $coidString : 'coid://'.$coidString
|
||||
);
|
||||
// Normalize scheme and host segments to lower case
|
||||
return new IRI('coid://'.strtolower($coidPre->getHost()).$coidPre->getPath());
|
||||
// Keep query and fragment (will currently make those IRIs invalid as COIDs
|
||||
// but may be used in future)
|
||||
return new IRI('coid://'.strtolower($coidPre->getHost()).$coidPre->getPath()
|
||||
.($coidPre->getQuery() !== null ? '?'.$coidPre->getQuery() : '')
|
||||
.($coidPre->getFragment() !== null ? '#'.$coidPre->getFragment() : ''));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,7 +51,8 @@ class COIDParser {
|
||||
*/
|
||||
public static function getType(IRI $coid) : ?int {
|
||||
if ($coid->getScheme()!='coid' || $coid->getHost()==''
|
||||
|| preg_match(self::REGEX_HOSTNAME, $coid->getHost()) != 1)
|
||||
|| preg_match(self::REGEX_HOSTNAME, $coid->getHost()) !== 1
|
||||
|| $coid->getQuery() !== null || $coid->getFragment() !== null)
|
||||
return self::COID_INVALID;
|
||||
|
||||
if ($coid->getPath()=='' || $coid->getPath()=='/')
|
||||
|
||||
@@ -11,7 +11,8 @@ use ML\IRI\IRI;
|
||||
use ML\JsonLD\JsonLD, ML\JsonLD\Node;
|
||||
use Psr\Log\LoggerInterface, Psr\Log\LoggerAwareTrait;
|
||||
use GuzzleHttp\ClientInterface, GuzzleHttp\Client, GuzzleHttp\HandlerStack;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use GuzzleHttp\Exception\ConnectException,
|
||||
GuzzleHttp\Exception\RequestException;
|
||||
use GuzzleHttp\Psr7\Request,
|
||||
GuzzleHttp\Psr7\Response;
|
||||
use Kevinrob\GuzzleCache\CacheMiddleware;
|
||||
@@ -41,6 +42,8 @@ class ObjectRetriever implements CustomCacheAndLogInterface {
|
||||
private $objects = [];
|
||||
private $defaultReader;
|
||||
private $customCacheDefaultRequest;
|
||||
private $fallbackStoreScope;
|
||||
private $permanentStoreScope;
|
||||
|
||||
const CO_API_URL = 'https://od.coid.link/';
|
||||
|
||||
@@ -50,15 +53,23 @@ class ObjectRetriever implements CustomCacheAndLogInterface {
|
||||
'cache_storage' => null,
|
||||
'cache_prefix' => 'clobj:',
|
||||
'cache_ttl' => 60,
|
||||
'static_config_path' => null,
|
||||
'static_object_directory' => null,
|
||||
'auth_ns' => null,
|
||||
'auth_secret' => null,
|
||||
'api_base_url' => null,
|
||||
'logger' => null,
|
||||
'timeout' => 20,
|
||||
'connect_timeout' => 5
|
||||
'connect_timeout' => 5,
|
||||
'fallback_store_directory' => null,
|
||||
'fallback_store_scope' => null,
|
||||
'permanent_store_directory' => null,
|
||||
'permanent_store_scope' => null
|
||||
], $options);
|
||||
|
||||
// Support deprecated name of static_object_directory
|
||||
if (!isset($this->options['static_object_directory']) && isset($options['static_config_path']))
|
||||
$this->options['static_object_directory'] = $options['static_config_path'];
|
||||
|
||||
// Check object cache configuration
|
||||
if (isset($this->options['cache_storage'])
|
||||
&& !($this->options['cache_storage'] instanceof CacheStorageInterface)
|
||||
@@ -76,6 +87,42 @@ class ObjectRetriever implements CustomCacheAndLogInterface {
|
||||
$this->cache = new Psr16CacheStorage($this->options['cache_storage']);
|
||||
}
|
||||
|
||||
// Check fallback store configuration
|
||||
if (isset($this->options['fallback_store_directory'])
|
||||
&& isset($this->options['fallback_store_scope'])
|
||||
&& !is_dir($this->options['fallback_store_directory'])
|
||||
) {
|
||||
throw new InvalidSDKConfigurationException('Invalid fallback_store_directory specified; must be an existing directory.');
|
||||
}
|
||||
|
||||
if (isset($this->options['fallback_store_directory'])
|
||||
&& isset($this->options['fallback_store_scope'])) {
|
||||
$this->fallbackStoreScope = is_array($this->options['fallback_store_scope'])
|
||||
? $this->options['fallback_store_scope']
|
||||
: array_map('trim', explode(',', $this->options['fallback_store_scope']));
|
||||
}
|
||||
|
||||
// Check permanent store configuration
|
||||
if (isset($this->options['permanent_store_directory'])
|
||||
&& isset($this->options['permanent_store_scope'])
|
||||
&& !is_dir($this->options['permanent_store_directory'])
|
||||
) {
|
||||
throw new InvalidSDKConfigurationException('Invalid permanent_store_directory specified; must be an existing directory.');
|
||||
}
|
||||
|
||||
if (isset($this->options['permanent_store_directory'])
|
||||
&& isset($this->options['permanent_store_scope'])) {
|
||||
$this->permanentStoreScope = is_array($this->options['permanent_store_scope'])
|
||||
? $this->options['permanent_store_scope']
|
||||
: array_map('trim', explode(',', $this->options['permanent_store_scope']));
|
||||
}
|
||||
|
||||
if (isset($this->fallbackStoreScope) && isset($this->permanentStoreScope)
|
||||
&& realpath($this->options['fallback_store_directory']) === realpath($this->options['permanent_store_directory'])
|
||||
) {
|
||||
throw new InvalidSDKConfigurationException('The permanent_store_directory and the fallback_store_directory must be different directories.');
|
||||
}
|
||||
|
||||
// Set up logger
|
||||
if (is_a($this->options['logger'], LoggerInterface::class)) {
|
||||
$this->setLogger($this->options['logger']);
|
||||
@@ -159,6 +206,84 @@ class ObjectRetriever implements CustomCacheAndLogInterface {
|
||||
|
||||
$this->cache->save($this->getCacheKey($id), $entry, $ttl);
|
||||
}
|
||||
|
||||
$this->putIntoFallbackStore($id, $data);
|
||||
$this->putIntoPermanentStore($id, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file location of an object in a store, or null if the store
|
||||
* is not configured or the object is out of scope.
|
||||
*/
|
||||
private function getStoreLocation($directory, ?array $scope, $id) : ?string {
|
||||
if (!isset($scope))
|
||||
return null;
|
||||
|
||||
// Only objects are stored, not custom data or attachments
|
||||
$coid = new IRI($id);
|
||||
if (!COIDParser::isValidCOID($coid))
|
||||
return null;
|
||||
|
||||
if (!in_array('*', $scope)
|
||||
&& !in_array($coid->getHost(), $scope))
|
||||
return null;
|
||||
|
||||
return $directory.DIRECTORY_SEPARATOR.
|
||||
$coid->getHost().str_replace('/', DIRECTORY_SEPARATOR, $coid->getPath())
|
||||
.DIRECTORY_SEPARATOR.'object.jsonld';
|
||||
}
|
||||
|
||||
private function putIntoStore($location, $id, $data, $storeName) {
|
||||
// Write to a temporary file and rename it so that concurrent readers
|
||||
// never see a partially written object
|
||||
$directory = dirname($location);
|
||||
$tempFile = $location.'.'.uniqid('', true).'.tmp';
|
||||
if ((!is_dir($directory) && !@mkdir($directory, 0777, true) && !is_dir($directory))
|
||||
|| @file_put_contents($tempFile, $data) === false
|
||||
|| !@rename($tempFile, $location)) {
|
||||
@unlink($tempFile);
|
||||
if (isset($this->logger))
|
||||
$this->logger->warning('Could not write <'.$id.'> to '.$storeName.' store.');
|
||||
}
|
||||
}
|
||||
|
||||
private function getFromStore($location) : ?string {
|
||||
if (!isset($location) || !is_file($location))
|
||||
return null;
|
||||
|
||||
$data = @file_get_contents($location);
|
||||
return ($data === false) ? null : $data;
|
||||
}
|
||||
|
||||
private function getFallbackStoreLocation($id) : ?string {
|
||||
return $this->getStoreLocation($this->options['fallback_store_directory'],
|
||||
$this->fallbackStoreScope, $id);
|
||||
}
|
||||
|
||||
private function putIntoFallbackStore($id, $data) {
|
||||
$location = $this->getFallbackStoreLocation($id);
|
||||
if (isset($location))
|
||||
$this->putIntoStore($location, $id, $data, 'fallback');
|
||||
}
|
||||
|
||||
private function getFromFallbackStore($id) : ?string {
|
||||
return $this->getFromStore($this->getFallbackStoreLocation($id));
|
||||
}
|
||||
|
||||
private function getPermanentStoreLocation($id) : ?string {
|
||||
return $this->getStoreLocation($this->options['permanent_store_directory'],
|
||||
$this->permanentStoreScope, $id);
|
||||
}
|
||||
|
||||
private function putIntoPermanentStore($id, $data) {
|
||||
// Objects in the permanent store are never overwritten
|
||||
$location = $this->getPermanentStoreLocation($id);
|
||||
if (isset($location) && !is_file($location))
|
||||
$this->putIntoStore($location, $id, $data, 'permanent');
|
||||
}
|
||||
|
||||
private function getFromPermanentStore($id) : ?string {
|
||||
return $this->getFromStore($this->getPermanentStoreLocation($id));
|
||||
}
|
||||
|
||||
public function getFromCacheCustom($id) {
|
||||
@@ -256,14 +381,14 @@ class ObjectRetriever implements CustomCacheAndLogInterface {
|
||||
return $this->objects[$uriString][1];
|
||||
}
|
||||
|
||||
if (isset($this->options['static_config_path'])) {
|
||||
$location = realpath($this->options['static_config_path'].DIRECTORY_SEPARATOR.
|
||||
if (isset($this->options['static_object_directory'])) {
|
||||
$location = realpath($this->options['static_object_directory'].DIRECTORY_SEPARATOR.
|
||||
$coid->getHost().str_replace('/', DIRECTORY_SEPARATOR, $coid->getPath())
|
||||
.DIRECTORY_SEPARATOR.'object.jsonld');
|
||||
|
||||
if ($location && file_exists($location)) {
|
||||
$object = $location;
|
||||
$this->logInfoWithTime('Fetched <'.$uriString.'> from static configuration.', $ts);
|
||||
$this->logInfoWithTime('Fetched <'.$uriString.'> from static object directory.', $ts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,6 +398,12 @@ class ObjectRetriever implements CustomCacheAndLogInterface {
|
||||
$this->logInfoWithTime('Fetched <'.$uriString.'> from object cache.', $ts);
|
||||
}
|
||||
|
||||
if (!isset($object)) {
|
||||
$object = $this->getFromPermanentStore($uriString);
|
||||
if (isset($object))
|
||||
$this->logInfoWithTime('Fetched <'.$uriString.'> from permanent store.', $ts);
|
||||
}
|
||||
|
||||
if (!isset($object)) {
|
||||
try {
|
||||
$response = $this->client
|
||||
@@ -282,21 +413,44 @@ class ObjectRetriever implements CustomCacheAndLogInterface {
|
||||
$object = (string)$response->getBody();
|
||||
$this->putIntoCache($uriString, $object, $this->options['cache_ttl']);
|
||||
$this->logInfoWithTime('Fetched <'.$uriString.'> from Core API ['.$response->getStatusCode().'].', $ts);
|
||||
} catch (ConnectException $e) {
|
||||
// connection failed or timed out
|
||||
$object = $this->getFromFallbackStore($uriString);
|
||||
if (!isset($object)) {
|
||||
$this->logInfoWithTime('Object <'.$uriString.'> could not be retrieved from Core API (connection failed).', $ts);
|
||||
throw new CoreAPIException($e->getMessage(), $e);
|
||||
}
|
||||
$this->logInfoWithTime('Fetched <'.$uriString.'> from fallback store (connection failed).', $ts);
|
||||
} catch (RequestException $e) {
|
||||
if ($e->hasResponse() && $e->getResponse()->getStatusCode() == 404) {
|
||||
// not found
|
||||
// not found (404)
|
||||
$this->logInfoWithTime('Object <'.$uriString.'> not found in Core API.', $ts);
|
||||
return null;
|
||||
} else
|
||||
if ($e->hasResponse()) {
|
||||
// other API error
|
||||
$this->logInfoWithTime('Object <'.$uriString.'> could not be retrieved from Core API ['.$e->getResponse()->getStatusCode().'].', $ts);
|
||||
if ($e->hasResponse() && $e->getResponse()->getStatusCode() >= 500) {
|
||||
// other API error (5xx)
|
||||
$object = $this->getFromFallbackStore($uriString);
|
||||
$statusCode = $e->getResponse()->getStatusCode();
|
||||
if (!isset($object)) {
|
||||
$this->logInfoWithTime('Object <'.$uriString.'> could not be retrieved from Core API ['.$statusCode.'].', $ts);
|
||||
throw new CoreAPIException($e->getMessage(), $e);
|
||||
} else {
|
||||
}
|
||||
$this->logInfoWithTime('Fetched <'.$uriString.'> from fallback store (Core API returned ['.$statusCode.']).', $ts);
|
||||
} else
|
||||
if (!$e->hasResponse()) {
|
||||
// API error without response
|
||||
$object = $this->getFromFallbackStore($uriString);
|
||||
if (!isset($object)) {
|
||||
$this->logInfoWithTime('Object <'.$uriString.'> could not be retrieved from Core API (no status code).', $ts);
|
||||
throw new CoreAPIException($e->getMessage(), $e);
|
||||
}
|
||||
$this->logInfoWithTime('Fetched <'.$uriString.'> from fallback store (Core API returned no status code).', $ts);
|
||||
} else {
|
||||
// other API error (e.g. 4xx)
|
||||
$statusCode = $e->getResponse()->getStatusCode();
|
||||
$this->logInfoWithTime('Object <'.$uriString.'> could not be retrieved from Core API ['.$statusCode.'].', $ts);
|
||||
throw new CoreAPIException($e->getMessage(), $e);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// other exceptions
|
||||
$this->logInfoWithTime('Object <'.$uriString.'> could not be retrieved from Core API (unexpected exception).', $ts);
|
||||
@@ -481,10 +635,10 @@ class ObjectRetriever implements CustomCacheAndLogInterface {
|
||||
*/
|
||||
public function get($coid) {
|
||||
if (is_string($coid))
|
||||
return $this->getObject(new IRI($coid));
|
||||
return $this->getObjectNode(new IRI($coid));
|
||||
|
||||
if (is_object($coid) && get_class($coid)=='ML\IRI\IRI')
|
||||
return $this->getObject($coid);
|
||||
return $this->getObjectNode($coid);
|
||||
|
||||
throw new SDKException('COID must be passed as a string or an IRI object.');
|
||||
}
|
||||
@@ -496,7 +650,7 @@ class ObjectRetriever implements CustomCacheAndLogInterface {
|
||||
* @param string $filename
|
||||
*/
|
||||
public function getAttachment(IRI $coid, $filename) {
|
||||
$object = $this->getObject($coid);
|
||||
$object = $this->getObjectNode($coid);
|
||||
|
||||
if (!$object)
|
||||
// Cannot get attachment for non-existing object
|
||||
@@ -526,14 +680,35 @@ class ObjectRetriever implements CustomCacheAndLogInterface {
|
||||
$this->putIntoCache($cacheId, $fileData, 0);
|
||||
|
||||
$this->logInfoWithTime('Fetched attachment <'.$filename.'> for <'.$object->getId().'> from Core API ['.$response->getStatusCode().'].', $ts);
|
||||
} catch (ConnectException $e) {
|
||||
// connection failed or timed out
|
||||
$this->logInfoWithTime('Attachment <'.$filename.'> for <'.$object->getId().'> could not be retrieved from Core API (connection failed).', $ts);
|
||||
throw new CoreAPIException($e->getMessage(), $e);
|
||||
} catch (RequestException $e) {
|
||||
if ($e->hasResponse() && $e->getResponse()->getStatusCode() == 404) {
|
||||
// not found (404)
|
||||
$this->logInfoWithTime('Attachment <'.$filename.'> for <'.$object->getId().'> not found in Core API.', $ts);
|
||||
return null;
|
||||
} else
|
||||
if ($e->hasResponse()) {
|
||||
// other API error (4xx, 5xx)
|
||||
$statusCode = $e->getResponse()->getStatusCode();
|
||||
$this->logInfoWithTime('Attachment <'.$filename.'> for <'.$object->getId().'> could not be retrieved from Core API ['.$statusCode.'].', $ts);
|
||||
throw new CoreAPIException($e->getMessage(), $e);
|
||||
} else {
|
||||
// API error without response
|
||||
$this->logInfoWithTime('Attachment <'.$filename.'> for <'.$object->getId().'> could not be retrieved from Core API (no status code).', $ts);
|
||||
throw new CoreAPIException($e->getMessage(), $e);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->logInfoWithTime('Attachment <'.$filename.'> for <'.$object->getId().'> not found in Core API ['.$e->getResponse()->getStatusCode().'].', $ts);
|
||||
// ignore exception - treat as non-existing file
|
||||
// other exceptions
|
||||
$this->logInfoWithTime('Attachment <'.$filename.'> for <'.$object->getId().'> could not be retrieved from Core API (unexpected exception).', $ts);
|
||||
throw new CoreAPIException($e->getMessage(), $e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $fileContent;
|
||||
return $fileContent ?? null;
|
||||
}
|
||||
|
||||
private function assertAuthenticatingNamespaceAndGetId() : IRI {
|
||||
@@ -555,7 +730,7 @@ class ObjectRetriever implements CustomCacheAndLogInterface {
|
||||
* @return Node
|
||||
*/
|
||||
public function getAuthenticatingNamespaceObject() : ?Node {
|
||||
return $this->getObject($this->assertAuthenticatingNamespaceAndGetId());
|
||||
return $this->getObjectNode($this->assertAuthenticatingNamespaceAndGetId());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -565,7 +740,7 @@ class ObjectRetriever implements CustomCacheAndLogInterface {
|
||||
* @return Node
|
||||
*/
|
||||
public function getAuthenticatingNamespaceObjectNode() : ?Node {
|
||||
return $this->getObject($this->assertAuthenticatingNamespaceAndGetId());
|
||||
return $this->getObjectNode($this->assertAuthenticatingNamespaceAndGetId());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,7 +16,9 @@ composer require cloudobjects/sdk
|
||||
|
||||
## Usage
|
||||
|
||||
The primary purpose of the SDK is the retrieval of object descriptions from the CloudObjects Core Object API. To do so, you create an instance of the `CloudObjects\SDK\ObjectRetriever` class. You can use this class directy, but you can also use the static facade (`CloudObjects\SDK\ObjectRetrieverFacade`) or the global function `cloudobjects_get_object()`, depending on the architecture of your PHP application and your preferred coding style. You can use the high-level `CloudObjects\SDK\CloudObject` interface or the lower-level `ML\JsonLD\Node` interface to work with the parsed JSON-LD representation of the object.
|
||||
The primary purpose of the SDK is the retrieval of object descriptions from the CloudObjects Core Object API. To do so, you create an instance of the `CloudObjects\SDK\ObjectRetriever` class. You can use this class directy, but you can also use the static facade (`CloudObjects\SDK\ObjectRetrieverFacade`) or the global function `cloudobjects_get_object()`, depending on the architecture of your PHP application and your preferred coding style.
|
||||
|
||||
You can use the high-level `CloudObjects\SDK\CloudObject` interface or the lower-level `ML\JsonLD\Node` interface (from the [sweetrdf/json-ld](https://packagist.org/packages/sweetrdf/json-ld) package) to work with the parsed JSON-LD representation of the object.
|
||||
|
||||
### Using the ObjectRetriever directly
|
||||
|
||||
@@ -29,6 +31,26 @@ $object = $retriever->getCloudObject(new IRI('coid://cloudobjects.io'));
|
||||
echo $object->getLabel();
|
||||
```
|
||||
|
||||
When no options are passed to the `ObjectRetriever`, it will initialize with default settings and no authentication, which means that it can only retrieve public triples from public objects. You can authenticate with a CloudObjects namespace (domain name) and the shared secret between this namespace and CloudObjects.
|
||||
|
||||
```php
|
||||
use CloudObjects\SDK\ObjectRetriever;
|
||||
use ML\IRI\IRI;
|
||||
|
||||
$retriever = new ObjectRetriever([
|
||||
'auth_ns' => 'example.com',
|
||||
'auth_secret' => 'YOUR_SHARED_SECRET',
|
||||
]);
|
||||
|
||||
$object = $retriever->getCloudObject(new IRI('coid://example.com/PrivateObject'));
|
||||
|
||||
// Retrieve the object describing the authenticating namespace itself
|
||||
$namespace = $retriever->getAuthenticatingNamespaceCloudObject();
|
||||
echo $namespace->getLabel();
|
||||
```
|
||||
|
||||
Any production usage should configure a cache; [all the caching-related features are documented here](docs/caching.md). All [configuration options are documented here](docs/options.md).
|
||||
|
||||
### Using the static facade
|
||||
|
||||
```php
|
||||
@@ -36,7 +58,7 @@ use CloudObjects\SDK\ObjectRetriever;
|
||||
use CloudObjects\SDK\ObjectRetrieverFacade;
|
||||
use ML\IRI\IRI;
|
||||
|
||||
ObjectRetrieverFacade::setObjectRetriever(new ObjectRetriever());
|
||||
ObjectRetrieverFacade::setObjectRetriever(new ObjectRetriever);
|
||||
|
||||
$object = ObjectRetrieverFacade::getCloudObject(new IRI('coid://cloudobjects.io'));
|
||||
echo $object->getLabel();
|
||||
@@ -50,7 +72,7 @@ The global function uses the static facade, so the facade must be initialized fi
|
||||
use CloudObjects\SDK\ObjectRetriever;
|
||||
use CloudObjects\SDK\ObjectRetrieverFacade;
|
||||
|
||||
ObjectRetrieverFacade::setObjectRetriever(new ObjectRetriever());
|
||||
ObjectRetrieverFacade::setObjectRetriever(new ObjectRetriever);
|
||||
|
||||
$object = cloudobjects_get_object('coid://cloudobjects.io');
|
||||
echo $object->getLabel();
|
||||
|
||||
+193
-1
@@ -1,9 +1,18 @@
|
||||
## Caching
|
||||
|
||||
# Standard Caching
|
||||
### Standard Caching
|
||||
|
||||
Object descriptions that are requested through the ObjectRetriever are fetched through the CloudObjects Core Object API. They are cached in-memory inside the class for a default of 60 seconds. This can be changed using the `cache_ttl` option, which takes a number of seconds.
|
||||
|
||||
```php
|
||||
use CloudObjects\SDK\ObjectRetriever;
|
||||
|
||||
// Keep objects in memory for 5 minutes
|
||||
$retriever = new ObjectRetriever([
|
||||
'cache_ttl' => 300,
|
||||
]);
|
||||
```
|
||||
|
||||
This in-class memory obviously only works as long as the class is alive and prevents subsequent requests for the same object hitting the API, but the cache will not exist between multiple requests handled in different processes. For this purpose, an external cache should be added.
|
||||
|
||||
To add an external cache, provide a storage using the `cache_storage` interface. The storage must be a class that implements either of the following interfaces:
|
||||
@@ -16,3 +25,186 @@ External storage is used twice:
|
||||
|
||||
- Storing the HTTP request according to the APIs caching headers.
|
||||
- Storing the object data according to `cache_ttl`.
|
||||
|
||||
All keys written to the external cache are prefixed with the value of the `cache_prefix` option (default: `clobj:`), so you can share a cache with other parts of your application.
|
||||
|
||||
The following examples use the [Symfony Cache component](https://symfony.com/doc/current/components/cache.html), but any library implementing one of the interfaces above works.
|
||||
|
||||
Using a PSR-6 cache pool, e.g. on the filesystem:
|
||||
|
||||
```php
|
||||
use CloudObjects\SDK\ObjectRetriever;
|
||||
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
|
||||
|
||||
$retriever = new ObjectRetriever([
|
||||
'cache_storage' => new FilesystemAdapter('cloudobjects', 0, '/var/cache/myapp'),
|
||||
'cache_ttl' => 300,
|
||||
]);
|
||||
```
|
||||
|
||||
Using a PSR-16 simple cache, e.g. in Redis:
|
||||
|
||||
```php
|
||||
use CloudObjects\SDK\ObjectRetriever;
|
||||
use Symfony\Component\Cache\Adapter\RedisAdapter;
|
||||
use Symfony\Component\Cache\Psr16Cache;
|
||||
|
||||
$redis = RedisAdapter::createConnection('redis://localhost');
|
||||
|
||||
$retriever = new ObjectRetriever([
|
||||
'cache_storage' => new Psr16Cache(new RedisAdapter($redis)),
|
||||
'cache_prefix' => 'myapp:clobj:',
|
||||
]);
|
||||
```
|
||||
|
||||
### Fallback Store
|
||||
|
||||
Fallback store is a mechanism that can be used by applications that require high availability but may lose connectivity to CloudObjects Core. With fallback store, a copy of an object is saved in a directory. If the API fails with a 5xx error, times out, or the request fails without a response, an `ObjectRetriever` will return the stored copy instead. Be advised that fallback storage will, once configured, result in your application not noticing when the Object API is unreachable - at least for objects in the store - and will continue to deliver the saved copy even if the original in CloudObjects Core is changed or removed.
|
||||
|
||||
It is possible for the fallback store to be configured for all objects or only for objects in specified namespaces. Since the SDK updates stored copies whenever it retrieves an object successfully but never deletes them, not even when the object has been removed from CloudObjects Core, the fallback store can grow large; hence it should be limited to specific namespaces in applications where many random objects may be retrieved. **Use this feature with caution and disable it if it causes any problems.**
|
||||
|
||||
To enable fallback store, set the `fallback_store_directory` option to a writeable path on the filesystem. Also, set `fallback_store_scope` to either the string `*` to save all retrieved objects, or a list of namespaces (domains only without `coid://` prefix); the list of namespaces can be provided as a comma-separated string or an array of strings.
|
||||
|
||||
Storing all retrieved objects:
|
||||
|
||||
```php
|
||||
use CloudObjects\SDK\ObjectRetriever;
|
||||
|
||||
$retriever = new ObjectRetriever([
|
||||
'fallback_store_directory' => '/var/lib/myapp/cloudobjects-fallback',
|
||||
'fallback_store_scope' => '*',
|
||||
]);
|
||||
```
|
||||
|
||||
Storing only objects from specific namespaces, provided as an array:
|
||||
|
||||
```php
|
||||
use CloudObjects\SDK\ObjectRetriever;
|
||||
|
||||
$retriever = new ObjectRetriever([
|
||||
'fallback_store_directory' => '/var/lib/myapp/cloudobjects-fallback',
|
||||
'fallback_store_scope' => [ 'example.com', 'cloudobjects.io' ],
|
||||
]);
|
||||
```
|
||||
|
||||
Or as a comma-separated string, which is convenient when the value comes from an environment variable:
|
||||
|
||||
```php
|
||||
use CloudObjects\SDK\ObjectRetriever;
|
||||
|
||||
$retriever = new ObjectRetriever([
|
||||
'fallback_store_directory' => getenv('FALLBACK_STORE_DIRECTORY'),
|
||||
'fallback_store_scope' => 'example.com,cloudobjects.io',
|
||||
]);
|
||||
```
|
||||
|
||||
The fallback store can be combined with an external cache. The cache is still consulted first; the fallback store only comes into play when an object is neither cached nor retrievable from the API.
|
||||
|
||||
```php
|
||||
use CloudObjects\SDK\ObjectRetriever;
|
||||
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
|
||||
|
||||
$retriever = new ObjectRetriever([
|
||||
'cache_storage' => new FilesystemAdapter('cloudobjects', 0, '/var/cache/myapp'),
|
||||
'cache_ttl' => 300,
|
||||
'fallback_store_directory' => '/var/lib/myapp/cloudobjects-fallback',
|
||||
'fallback_store_scope' => 'example.com',
|
||||
]);
|
||||
```
|
||||
|
||||
### Permanent Store
|
||||
|
||||
Permanent store is a mechanism that can be used by applications that want to minimize connections to CloudObjects Core, may go offline after initialization, or want to work with the same version of an object for the lifetime of their run without getting updates. With permanent store, an `ObjectRetriever` saves a copy of an object in a directory the first time it retrieves it from the Object API. Once an object is in the permanent store, the API will _never_ be contacted for this object again. Be advised that permanent storage will, once configured, result in your application continuing to deliver the saved copy even if the original in CloudObjects Core is changed or removed, and not noticing when the Object API is unreachable - at least for objects in the store.
|
||||
|
||||
It is possible for the permanent store to be configured for all objects or only for objects in specified namespaces. Since the permanent store is never purged or refreshed by the SDK, it can grow large; hence it should be limited to specific namespaces in applications where many random objects may be retrieved. To get the latest version of an object, delete its copy from the directory, or clear the directory entirely. **Use this feature with caution and disable it if it causes any problems.**
|
||||
|
||||
To enable permanent store, set the `permanent_store_directory` option to a writeable path on the filesystem. Also, set `permanent_store_scope` to either the string `*` to save all retrieved objects, or a list of namespaces (domains only without `coid://` prefix); the list of namespaces can be provided as a comma-separated string or an array of strings.
|
||||
|
||||
Storing all retrieved objects:
|
||||
|
||||
```php
|
||||
use CloudObjects\SDK\ObjectRetriever;
|
||||
|
||||
$retriever = new ObjectRetriever([
|
||||
'permanent_store_directory' => '/var/lib/myapp/cloudobjects-permanent',
|
||||
'permanent_store_scope' => '*',
|
||||
]);
|
||||
```
|
||||
|
||||
Storing only objects from specific namespaces, provided as an array:
|
||||
|
||||
```php
|
||||
use CloudObjects\SDK\ObjectRetriever;
|
||||
|
||||
$retriever = new ObjectRetriever([
|
||||
'permanent_store_directory' => '/var/lib/myapp/cloudobjects-permanent',
|
||||
'permanent_store_scope' => [ 'example.com', 'cloudobjects.io' ],
|
||||
]);
|
||||
```
|
||||
|
||||
Or as a comma-separated string, which is convenient when the value comes from an environment variable:
|
||||
|
||||
```php
|
||||
use CloudObjects\SDK\ObjectRetriever;
|
||||
|
||||
$retriever = new ObjectRetriever([
|
||||
'permanent_store_directory' => getenv('PERMANENT_STORE_DIRECTORY'),
|
||||
'permanent_store_scope' => 'example.com,cloudobjects.io',
|
||||
]);
|
||||
```
|
||||
|
||||
The permanent store can be combined with an external cache. The cache is still consulted first, as it may be faster than the filesystem; the permanent store only comes into play when an object is not cached, and the API is only contacted when the object is in neither.
|
||||
|
||||
```php
|
||||
use CloudObjects\SDK\ObjectRetriever;
|
||||
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
|
||||
|
||||
$retriever = new ObjectRetriever([
|
||||
'cache_storage' => new FilesystemAdapter('cloudobjects', 0, '/var/cache/myapp'),
|
||||
'cache_ttl' => 300,
|
||||
'permanent_store_directory' => '/var/lib/myapp/cloudobjects-permanent',
|
||||
'permanent_store_scope' => 'example.com',
|
||||
]);
|
||||
```
|
||||
|
||||
Permanent store and fallback store can be combined, for example to pin objects from your own namespace while keeping a fallback copy of everything else. Both stores must use separate directories, otherwise the `ObjectRetriever` throws an `InvalidSDKConfigurationException`. If an object is in scope of both, the permanent store takes precedence.
|
||||
|
||||
```php
|
||||
use CloudObjects\SDK\ObjectRetriever;
|
||||
|
||||
$retriever = new ObjectRetriever([
|
||||
'permanent_store_directory' => '/var/lib/myapp/cloudobjects-permanent',
|
||||
'permanent_store_scope' => 'example.com',
|
||||
'fallback_store_directory' => '/var/lib/myapp/cloudobjects-fallback',
|
||||
'fallback_store_scope' => '*',
|
||||
]);
|
||||
```
|
||||
|
||||
Unlike the [static object directory](#static-objects), which contains object descriptions that you provide yourself, the permanent store is filled automatically by the SDK from the Object API.
|
||||
|
||||
### Static Objects
|
||||
|
||||
The static object directory is a mechanism that is mostly intended for testing. It allows you to provide object descriptions yourself, for example as fixtures in your test suite, so that your code can be tested without connecting to CloudObjects Core, or with objects that differ from or do not yet exist in CloudObjects Core. It should usually not be configured in production.
|
||||
|
||||
To enable it, set the `static_object_directory` option to a path on the filesystem. Object descriptions are read from `<directory>/<namespace>/<path of COID>/object.jsonld`, which is the same layout used by the fallback and permanent stores. For example, the object `coid://example.com/MyObject` is read from `<directory>/example.com/MyObject/object.jsonld`, and the namespace object `coid://example.com` itself is read from `<directory>/example.com/object.jsonld`. Each file must contain the object description in JSON-LD format, with an `@id` that matches the COID.
|
||||
|
||||
```json
|
||||
{
|
||||
"@id": "coid://example.com/MyObject",
|
||||
"http://www.w3.org/2000/01/rdf-schema#label": "My Object"
|
||||
}
|
||||
```
|
||||
|
||||
The static object directory is consulted before the external cache, the permanent store and the API. Objects that are not found in the directory are retrieved as usual, so you can override only some objects and let the SDK fetch the rest:
|
||||
|
||||
```php
|
||||
use CloudObjects\SDK\ObjectRetriever;
|
||||
|
||||
$retriever = new ObjectRetriever([
|
||||
'static_object_directory' => __DIR__.'/fixtures/objects',
|
||||
]);
|
||||
```
|
||||
|
||||
Static objects are only used by `getObjectNode()` and the methods built on it; methods that fetch multiple objects from a namespace, such as `fetchAllObjectsInNamespace()`, always use the API. The SDK never writes to the static object directory, so it is not necessary to specify a scope; any COIDs will be considered for the static store.
|
||||
|
||||
In earlier versions of the SDK, this option was called `static_config_path`. The old name is deprecated but still supported.
|
||||
@@ -0,0 +1,20 @@
|
||||
## Options
|
||||
|
||||
The `ObjectRetriever` constructor accepts an associative array of options. All options are optional; any option not specified uses its default value.
|
||||
|
||||
| Key | Description | Default value |
|
||||
| --- | --- | --- |
|
||||
| `cache_storage` | External cache storage; must implement `Kevinrob\GuzzleCache\Storage\CacheStorageInterface`, `Psr\Cache\CacheItemPoolInterface` or `Psr\SimpleCache\CacheInterface`. See [caching](caching.md). | `null` |
|
||||
| `cache_prefix` | Prefix for all keys written to the external cache. | `'clobj:'` |
|
||||
| `cache_ttl` | Number of seconds that object descriptions are cached, both in memory and in the external cache. | `60` |
|
||||
| `static_object_directory` | Directory with static object descriptions, which are used instead of the API; mostly useful for testing. Objects are read from `<directory>/<namespace>/<path of COID>/object.jsonld`. Replaces the deprecated `static_config_path`. See [caching](caching.md#static-objects). | `null` |
|
||||
| `auth_ns` | Hostname of the namespace to authenticate as, e.g. `example.com`. | `null` |
|
||||
| `auth_secret` | Shared secret between the authenticating namespace and CloudObjects. | `null` |
|
||||
| `api_base_url` | Base URL of the CloudObjects Core Object API. If not set, `https://od.coid.link/` is used. | `null` |
|
||||
| `logger` | Logger implementing `Psr\Log\LoggerInterface`, which receives info messages about object retrieval. | `null` |
|
||||
| `timeout` | Total timeout for API requests, in seconds. | `20` |
|
||||
| `connect_timeout` | Timeout for establishing a connection to the API, in seconds. | `5` |
|
||||
| `fallback_store_directory` | Writeable directory in which copies of retrieved objects are saved and returned when the API fails with a 5xx error, times out, or the request fails without a response. Requires `fallback_store_scope`. See [caching](caching.md#fallback-store). | `null` |
|
||||
| `fallback_store_scope` | Which objects are saved in the fallback store: `'*'` for all objects, or a list of namespaces (domains without `coid://` prefix) as a comma-separated string or an array of strings. | `null` |
|
||||
| `permanent_store_directory` | Writeable directory in which copies of retrieved objects are saved; once an object is stored there, it is always returned from the store and the API is never contacted for it again. Requires `permanent_store_scope` and must differ from `fallback_store_directory`. See [caching](caching.md#permanent-store). | `null` |
|
||||
| `permanent_store_scope` | Which objects are saved in the permanent store: `'*'` for all objects, or a list of namespaces (domains without `coid://` prefix) as a comma-separated string or an array of strings. | `null` |
|
||||
@@ -50,6 +50,24 @@ class COIDParserTest extends \PHPUnit\Framework\TestCase {
|
||||
|
||||
$coid = new IRI('coid://exAMPle.CoM');
|
||||
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
|
||||
|
||||
$coid = new IRI('coid://example.com#hash=something');
|
||||
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
|
||||
|
||||
$coid = new IRI('coid://example.com?query=something');
|
||||
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
|
||||
|
||||
$coid = new IRI('coid://example.com/Example/1.0?query=something');
|
||||
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
|
||||
|
||||
$coid = new IRI('coid://example.com/Example#hash');
|
||||
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
|
||||
|
||||
$coid = new IRI('coid://example.com?');
|
||||
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
|
||||
|
||||
$coid = new IRI('coid://example.com#');
|
||||
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
|
||||
}
|
||||
|
||||
public function testUnversionedCOID() {
|
||||
@@ -145,4 +163,20 @@ class COIDParserTest extends \PHPUnit\Framework\TestCase {
|
||||
$this->assertFalse($coid1->equals($coid4));
|
||||
}
|
||||
|
||||
public function testQueryAndFragmentFromString() {
|
||||
$coid = COIDParser::fromString('example.com/Example?query=something');
|
||||
$this->assertEquals('coid://example.com/Example?query=something', (string)$coid);
|
||||
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
|
||||
|
||||
$coid = COIDParser::fromString('COID://Example.com/Example/1.0#hash');
|
||||
$this->assertEquals('coid://example.com/Example/1.0#hash', (string)$coid);
|
||||
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
|
||||
|
||||
$coid = COIDParser::fromString('example.com?');
|
||||
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
|
||||
|
||||
$coid = COIDParser::fromString('example.com#');
|
||||
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,17 +8,23 @@ namespace CloudObjects\SDK;
|
||||
|
||||
use ML\IRI\IRI;
|
||||
use GuzzleHttp\Client, GuzzleHttp\Handler\MockHandler,
|
||||
GuzzleHttp\HandlerStack, GuzzleHttp\Psr7\Response;
|
||||
GuzzleHttp\HandlerStack, GuzzleHttp\Psr7\Request, GuzzleHttp\Psr7\Response;
|
||||
use GuzzleHttp\Exception\ConnectException, GuzzleHttp\Exception\RequestException;
|
||||
use CloudObjects\SDK\Exceptions\CoreAPIException;
|
||||
|
||||
class ObjectRetrieverMockTest extends \PHPUnit\Framework\TestCase {
|
||||
|
||||
private $retriever;
|
||||
|
||||
private function setMockResponse(Response $response) {
|
||||
$mock = new MockHandler([$response]);
|
||||
/**
|
||||
* @param Response|\Throwable ...$responses Responses in the order they are returned,
|
||||
* or exceptions to throw instead.
|
||||
*/
|
||||
private function setMockResponse(...$responses) : MockHandler {
|
||||
$mock = new MockHandler($responses);
|
||||
$handler = HandlerStack::create($mock);
|
||||
$this->retriever->setClient(new Client(['handler' => $handler]));
|
||||
return $mock;
|
||||
}
|
||||
|
||||
protected function setUp(): void {
|
||||
@@ -70,4 +76,405 @@ class ObjectRetrieverMockTest extends \PHPUnit\Framework\TestCase {
|
||||
$this->retriever->getObjectNode($coid);
|
||||
}
|
||||
|
||||
public function testWhenAPITimesOut() {
|
||||
$this->setMockResponse(new ConnectException(
|
||||
'cURL error 28: Operation timed out',
|
||||
new Request('GET', 'cloudobjects.io')
|
||||
));
|
||||
|
||||
$coid = new IRI('coid://cloudobjects.io');
|
||||
|
||||
try {
|
||||
$this->retriever->getObjectNode($coid);
|
||||
$this->fail('Expected CoreAPIException was not thrown.');
|
||||
} catch (CoreAPIException $e) {
|
||||
$this->assertInstanceOf(ConnectException::class, $e->getPrevious());
|
||||
}
|
||||
}
|
||||
|
||||
public function testGetAttachment() {
|
||||
$mock = $this->setMockResponse(
|
||||
new Response(200, ['Content-Type' => 'application/ld+json'],
|
||||
'{"@id":"coid://cloudobjects.io/Test","coid://cloudobjects.io/isAtRevision":"1-abc"}'),
|
||||
new Response(200, ['Content-Type' => 'text/plain'], 'Attachment content')
|
||||
);
|
||||
|
||||
$content = $this->retriever->getAttachment(new IRI('coid://cloudobjects.io/Test'), 'file.txt');
|
||||
$this->assertEquals('Attachment content', $content);
|
||||
$this->assertStringEndsWith('cloudobjects.io/Test/file.txt', $mock->getLastRequest()->getUri()->getPath());
|
||||
}
|
||||
|
||||
public function testGetMissingAttachment() {
|
||||
$this->setMockResponse(
|
||||
new Response(200, ['Content-Type' => 'application/ld+json'],
|
||||
'{"@id":"coid://cloudobjects.io/Test","coid://cloudobjects.io/isAtRevision":"1-abc"}'),
|
||||
new Response(404)
|
||||
);
|
||||
|
||||
$content = $this->retriever->getAttachment(new IRI('coid://cloudobjects.io/Test'), 'missing.txt');
|
||||
$this->assertNull($content);
|
||||
}
|
||||
|
||||
public function testWhenAPIThrowsErrorForAttachment() {
|
||||
$this->setMockResponse(
|
||||
new Response(200, ['Content-Type' => 'application/ld+json'],
|
||||
'{"@id":"coid://cloudobjects.io/Test","coid://cloudobjects.io/isAtRevision":"1-abc"}'),
|
||||
new Response(500)
|
||||
);
|
||||
|
||||
$this->expectException(CoreAPIException::class);
|
||||
$this->retriever->getAttachment(new IRI('coid://cloudobjects.io/Test'), 'file.txt');
|
||||
}
|
||||
|
||||
public function testWhenAPITimesOutForAttachment() {
|
||||
$this->setMockResponse(
|
||||
new Response(200, ['Content-Type' => 'application/ld+json'],
|
||||
'{"@id":"coid://cloudobjects.io/Test","coid://cloudobjects.io/isAtRevision":"1-abc"}'),
|
||||
new ConnectException(
|
||||
'cURL error 28: Operation timed out',
|
||||
new Request('GET', 'cloudobjects.io')
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
$this->retriever->getAttachment(new IRI('coid://cloudobjects.io/Test'), 'file.txt');
|
||||
$this->fail('Expected CoreAPIException was not thrown.');
|
||||
} catch (CoreAPIException $e) {
|
||||
$this->assertInstanceOf(ConnectException::class, $e->getPrevious());
|
||||
}
|
||||
}
|
||||
|
||||
private function createFallbackStoreDirectory() : string {
|
||||
$directory = sys_get_temp_dir().DIRECTORY_SEPARATOR.'clobj-fallback-'.uniqid();
|
||||
mkdir($directory);
|
||||
return $directory;
|
||||
}
|
||||
|
||||
private function removeDirectory(string $directory) {
|
||||
$files = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
foreach ($files as $file)
|
||||
$file->isDir() ? rmdir($file->getPathname()) : unlink($file->getPathname());
|
||||
rmdir($directory);
|
||||
}
|
||||
|
||||
public function testFallbackStoreSavesObjectInScope() {
|
||||
$directory = $this->createFallbackStoreDirectory();
|
||||
$this->retriever = new ObjectRetriever([
|
||||
'fallback_store_directory' => $directory,
|
||||
'fallback_store_scope' => 'example.com, cloudobjects.io',
|
||||
]);
|
||||
$body = '{"@id":"coid://cloudobjects.io/Test","http://www.w3.org/2000/01/rdf-schema#label":"Test"}';
|
||||
$this->setMockResponse(new Response(200, ['Content-Type' => 'application/ld+json'], $body));
|
||||
|
||||
try {
|
||||
$this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test'));
|
||||
|
||||
$location = $directory.'/cloudobjects.io/Test/object.jsonld';
|
||||
$this->assertFileExists($location);
|
||||
$this->assertEquals($body, file_get_contents($location));
|
||||
} finally {
|
||||
$this->removeDirectory($directory);
|
||||
}
|
||||
}
|
||||
|
||||
public function testFallbackStoreSkipsObjectOutOfScope() {
|
||||
$directory = $this->createFallbackStoreDirectory();
|
||||
$this->retriever = new ObjectRetriever([
|
||||
'fallback_store_directory' => $directory,
|
||||
'fallback_store_scope' => [ 'example.com' ],
|
||||
]);
|
||||
$this->setMockResponse(new Response(200, ['Content-Type' => 'application/ld+json'],
|
||||
'{"@id":"coid://cloudobjects.io/Test"}'));
|
||||
|
||||
try {
|
||||
$this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test'));
|
||||
$this->assertFileDoesNotExist($directory.'/cloudobjects.io/Test/object.jsonld');
|
||||
} finally {
|
||||
$this->removeDirectory($directory);
|
||||
}
|
||||
}
|
||||
|
||||
public function testFallbackStoreSavesAllObjectsWithWildcard() {
|
||||
$directory = $this->createFallbackStoreDirectory();
|
||||
$this->retriever = new ObjectRetriever([
|
||||
'fallback_store_directory' => $directory,
|
||||
'fallback_store_scope' => '*',
|
||||
]);
|
||||
$this->setMockResponse(new Response(200, ['Content-Type' => 'application/ld+json'],
|
||||
'{"@id":"coid://cloudobjects.io"}'));
|
||||
|
||||
try {
|
||||
$this->retriever->getObjectNode(new IRI('coid://cloudobjects.io'));
|
||||
$this->assertFileExists($directory.'/cloudobjects.io/object.jsonld');
|
||||
} finally {
|
||||
$this->removeDirectory($directory);
|
||||
}
|
||||
}
|
||||
|
||||
private function createRetrieverWithStoredObject(string $directory) {
|
||||
$this->retriever = new ObjectRetriever([
|
||||
'fallback_store_directory' => $directory,
|
||||
'fallback_store_scope' => '*',
|
||||
]);
|
||||
mkdir($directory.'/cloudobjects.io/Test', 0777, true);
|
||||
file_put_contents($directory.'/cloudobjects.io/Test/object.jsonld',
|
||||
'{"@id":"coid://cloudobjects.io/Test","http://www.w3.org/2000/01/rdf-schema#label":"Stored"}');
|
||||
}
|
||||
|
||||
public function testFallbackStoreUsedWhenAPIReturns500() {
|
||||
$directory = $this->createFallbackStoreDirectory();
|
||||
$this->createRetrieverWithStoredObject($directory);
|
||||
$this->setMockResponse(new Response(500));
|
||||
|
||||
try {
|
||||
$object = $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test'));
|
||||
$this->assertNotNull($object);
|
||||
$this->assertEquals('Stored', $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue());
|
||||
} finally {
|
||||
$this->removeDirectory($directory);
|
||||
}
|
||||
}
|
||||
|
||||
public function testFallbackStoreUsedWhenAPITimesOut() {
|
||||
$directory = $this->createFallbackStoreDirectory();
|
||||
$this->createRetrieverWithStoredObject($directory);
|
||||
$this->setMockResponse(new ConnectException(
|
||||
'cURL error 28: Operation timed out',
|
||||
new Request('GET', 'cloudobjects.io')
|
||||
));
|
||||
|
||||
try {
|
||||
$object = $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test'));
|
||||
$this->assertNotNull($object);
|
||||
$this->assertEquals('Stored', $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue());
|
||||
} finally {
|
||||
$this->removeDirectory($directory);
|
||||
}
|
||||
}
|
||||
|
||||
public function testFallbackStoreUsedWhenRequestFailsWithoutResponse() {
|
||||
$directory = $this->createFallbackStoreDirectory();
|
||||
$this->createRetrieverWithStoredObject($directory);
|
||||
$this->setMockResponse(new RequestException(
|
||||
'Request failed',
|
||||
new Request('GET', 'cloudobjects.io')
|
||||
));
|
||||
|
||||
try {
|
||||
$object = $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test'));
|
||||
$this->assertNotNull($object);
|
||||
$this->assertEquals('Stored', $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue());
|
||||
} finally {
|
||||
$this->removeDirectory($directory);
|
||||
}
|
||||
}
|
||||
|
||||
public function testErrorWhenRequestFailsWithoutResponseAndObjectNotInFallbackStore() {
|
||||
$directory = $this->createFallbackStoreDirectory();
|
||||
$this->retriever = new ObjectRetriever([
|
||||
'fallback_store_directory' => $directory,
|
||||
'fallback_store_scope' => '*',
|
||||
]);
|
||||
$this->setMockResponse(new RequestException(
|
||||
'Request failed',
|
||||
new Request('GET', 'cloudobjects.io')
|
||||
));
|
||||
|
||||
try {
|
||||
$this->expectException(CoreAPIException::class);
|
||||
$this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test'));
|
||||
} finally {
|
||||
$this->removeDirectory($directory);
|
||||
}
|
||||
}
|
||||
|
||||
public function testFallbackStoreNotUsedOnClientError() {
|
||||
$directory = $this->createFallbackStoreDirectory();
|
||||
$this->createRetrieverWithStoredObject($directory);
|
||||
$this->setMockResponse(new Response(403));
|
||||
|
||||
try {
|
||||
$this->expectException(CoreAPIException::class);
|
||||
$this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test'));
|
||||
} finally {
|
||||
$this->removeDirectory($directory);
|
||||
}
|
||||
}
|
||||
|
||||
public function testErrorWhenAPIThrowsErrorAndObjectNotInFallbackStore() {
|
||||
$directory = $this->createFallbackStoreDirectory();
|
||||
$this->retriever = new ObjectRetriever([
|
||||
'fallback_store_directory' => $directory,
|
||||
'fallback_store_scope' => '*',
|
||||
]);
|
||||
$this->setMockResponse(new Response(500));
|
||||
|
||||
try {
|
||||
$this->expectException(CoreAPIException::class);
|
||||
$this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test'));
|
||||
} finally {
|
||||
$this->removeDirectory($directory);
|
||||
}
|
||||
}
|
||||
|
||||
private function createRetrieverWithPermanentObject(string $directory, array $options = []) {
|
||||
$this->retriever = new ObjectRetriever(array_merge([
|
||||
'permanent_store_directory' => $directory,
|
||||
'permanent_store_scope' => '*',
|
||||
], $options));
|
||||
mkdir($directory.'/cloudobjects.io/Test', 0777, true);
|
||||
file_put_contents($directory.'/cloudobjects.io/Test/object.jsonld',
|
||||
'{"@id":"coid://cloudobjects.io/Test","http://www.w3.org/2000/01/rdf-schema#label":"Stored"}');
|
||||
}
|
||||
|
||||
public function testPermanentStoreSavesObjectInScope() {
|
||||
$directory = $this->createFallbackStoreDirectory();
|
||||
$this->retriever = new ObjectRetriever([
|
||||
'permanent_store_directory' => $directory,
|
||||
'permanent_store_scope' => 'example.com, cloudobjects.io',
|
||||
]);
|
||||
$body = '{"@id":"coid://cloudobjects.io/Test","http://www.w3.org/2000/01/rdf-schema#label":"Test"}';
|
||||
$this->setMockResponse(new Response(200, ['Content-Type' => 'application/ld+json'], $body));
|
||||
|
||||
try {
|
||||
$this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test'));
|
||||
|
||||
$location = $directory.'/cloudobjects.io/Test/object.jsonld';
|
||||
$this->assertFileExists($location);
|
||||
$this->assertEquals($body, file_get_contents($location));
|
||||
} finally {
|
||||
$this->removeDirectory($directory);
|
||||
}
|
||||
}
|
||||
|
||||
public function testPermanentStoreSkipsObjectOutOfScope() {
|
||||
$directory = $this->createFallbackStoreDirectory();
|
||||
$this->retriever = new ObjectRetriever([
|
||||
'permanent_store_directory' => $directory,
|
||||
'permanent_store_scope' => [ 'example.com' ],
|
||||
]);
|
||||
$this->setMockResponse(new Response(200, ['Content-Type' => 'application/ld+json'],
|
||||
'{"@id":"coid://cloudobjects.io/Test"}'));
|
||||
|
||||
try {
|
||||
$this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test'));
|
||||
$this->assertFileDoesNotExist($directory.'/cloudobjects.io/Test/object.jsonld');
|
||||
} finally {
|
||||
$this->removeDirectory($directory);
|
||||
}
|
||||
}
|
||||
|
||||
public function testPermanentStoreUsedWithoutContactingAPI() {
|
||||
$directory = $this->createFallbackStoreDirectory();
|
||||
$this->createRetrieverWithPermanentObject($directory);
|
||||
$mock = new MockHandler([ new Response(500) ]);
|
||||
$this->retriever->setClient(new Client(['handler' => HandlerStack::create($mock)]));
|
||||
|
||||
try {
|
||||
$object = $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test'));
|
||||
$this->assertNotNull($object);
|
||||
$this->assertEquals('Stored', $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue());
|
||||
// The mocked response has not been consumed
|
||||
$this->assertCount(1, $mock);
|
||||
} finally {
|
||||
$this->removeDirectory($directory);
|
||||
}
|
||||
}
|
||||
|
||||
public function testPermanentStoreNotOverwritten() {
|
||||
$directory = $this->createFallbackStoreDirectory();
|
||||
$this->createRetrieverWithPermanentObject($directory);
|
||||
$this->setMockResponse(new Response(200, ['Content-Type' => 'application/ld+json'],
|
||||
'[{"@id":"coid://cloudobjects.io/Test","http://www.w3.org/2000/01/rdf-schema#label":"Updated"}]'));
|
||||
|
||||
try {
|
||||
$this->retriever->fetchAllObjectsInNamespace(new IRI('coid://cloudobjects.io'));
|
||||
$this->assertStringContainsString('Stored',
|
||||
file_get_contents($directory.'/cloudobjects.io/Test/object.jsonld'));
|
||||
} finally {
|
||||
$this->removeDirectory($directory);
|
||||
}
|
||||
}
|
||||
|
||||
public function testPermanentStoreTakesPrecedenceOverFallbackStore() {
|
||||
$permanentDirectory = $this->createFallbackStoreDirectory();
|
||||
$fallbackDirectory = $this->createFallbackStoreDirectory();
|
||||
$this->createRetrieverWithPermanentObject($permanentDirectory, [
|
||||
'fallback_store_directory' => $fallbackDirectory,
|
||||
'fallback_store_scope' => '*',
|
||||
]);
|
||||
mkdir($fallbackDirectory.'/cloudobjects.io/Test', 0777, true);
|
||||
file_put_contents($fallbackDirectory.'/cloudobjects.io/Test/object.jsonld',
|
||||
'{"@id":"coid://cloudobjects.io/Test","http://www.w3.org/2000/01/rdf-schema#label":"Fallback"}');
|
||||
$this->setMockResponse(new Response(500));
|
||||
|
||||
try {
|
||||
$object = $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test'));
|
||||
$this->assertEquals('Stored', $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue());
|
||||
} finally {
|
||||
$this->removeDirectory($permanentDirectory);
|
||||
$this->removeDirectory($fallbackDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
private function createStaticObjectDirectory() : string {
|
||||
$directory = $this->createFallbackStoreDirectory();
|
||||
mkdir($directory.'/cloudobjects.io/Test', 0777, true);
|
||||
file_put_contents($directory.'/cloudobjects.io/Test/object.jsonld',
|
||||
'{"@id":"coid://cloudobjects.io/Test","http://www.w3.org/2000/01/rdf-schema#label":"Static"}');
|
||||
return $directory;
|
||||
}
|
||||
|
||||
public function testStaticObjectDirectoryUsedWithoutContactingAPI() {
|
||||
$directory = $this->createStaticObjectDirectory();
|
||||
$this->retriever = new ObjectRetriever([
|
||||
'static_object_directory' => $directory,
|
||||
]);
|
||||
$mock = new MockHandler([ new Response(500) ]);
|
||||
$this->retriever->setClient(new Client(['handler' => HandlerStack::create($mock)]));
|
||||
|
||||
try {
|
||||
$object = $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test'));
|
||||
$this->assertEquals('Static', $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue());
|
||||
// The mocked response has not been consumed
|
||||
$this->assertCount(1, $mock);
|
||||
} finally {
|
||||
$this->removeDirectory($directory);
|
||||
}
|
||||
}
|
||||
|
||||
public function testStaticObjectDirectoryWithDeprecatedOptionName() {
|
||||
$directory = $this->createStaticObjectDirectory();
|
||||
$this->retriever = new ObjectRetriever([
|
||||
'static_config_path' => $directory,
|
||||
]);
|
||||
$this->setMockResponse(new Response(500));
|
||||
|
||||
try {
|
||||
$object = $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test'));
|
||||
$this->assertEquals('Static', $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue());
|
||||
} finally {
|
||||
$this->removeDirectory($directory);
|
||||
}
|
||||
}
|
||||
|
||||
public function testStaticObjectDirectoryFallsThroughToAPI() {
|
||||
$directory = $this->createStaticObjectDirectory();
|
||||
$this->retriever = new ObjectRetriever([
|
||||
'static_object_directory' => $directory,
|
||||
]);
|
||||
$this->setMockResponse(new Response(200, ['Content-Type' => 'application/ld+json'],
|
||||
'{"@id":"coid://cloudobjects.io/Other","http://www.w3.org/2000/01/rdf-schema#label":"API"}'));
|
||||
|
||||
try {
|
||||
$object = $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Other'));
|
||||
$this->assertEquals('API', $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue());
|
||||
} finally {
|
||||
$this->removeDirectory($directory);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
namespace CloudObjects\SDK;
|
||||
|
||||
use CloudObjects\SDK\Exceptions\InvalidSDKConfigurationException;
|
||||
|
||||
class ObjectRetrieverTest extends \PHPUnit\Framework\TestCase {
|
||||
|
||||
public function testFallbackStoreWithExistingDirectory() {
|
||||
$retriever = new ObjectRetriever([
|
||||
'fallback_store_directory' => sys_get_temp_dir(),
|
||||
'fallback_store_scope' => '*',
|
||||
]);
|
||||
$this->assertInstanceOf(ObjectRetriever::class, $retriever);
|
||||
}
|
||||
|
||||
public function testFallbackStoreWithMissingDirectory() {
|
||||
$this->expectException(InvalidSDKConfigurationException::class);
|
||||
new ObjectRetriever([
|
||||
'fallback_store_directory' => sys_get_temp_dir().'/does-not-exist-'.uniqid(),
|
||||
'fallback_store_scope' => '*',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testPermanentStoreWithExistingDirectory() {
|
||||
$retriever = new ObjectRetriever([
|
||||
'permanent_store_directory' => sys_get_temp_dir(),
|
||||
'permanent_store_scope' => '*',
|
||||
]);
|
||||
$this->assertInstanceOf(ObjectRetriever::class, $retriever);
|
||||
}
|
||||
|
||||
public function testPermanentStoreWithMissingDirectory() {
|
||||
$this->expectException(InvalidSDKConfigurationException::class);
|
||||
new ObjectRetriever([
|
||||
'permanent_store_directory' => sys_get_temp_dir().'/does-not-exist-'.uniqid(),
|
||||
'permanent_store_scope' => '*',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testPermanentStoreWithSameDirectoryAsFallbackStore() {
|
||||
$this->expectException(InvalidSDKConfigurationException::class);
|
||||
new ObjectRetriever([
|
||||
'fallback_store_directory' => sys_get_temp_dir(),
|
||||
'fallback_store_scope' => '*',
|
||||
'permanent_store_directory' => sys_get_temp_dir(),
|
||||
'permanent_store_scope' => '*',
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -51,4 +51,34 @@ class ObjectRetrieverCacheTest extends \PHPUnit\Framework\TestCase {
|
||||
$this->assertEquals($object2->getRevision(), $object3->getRevision());
|
||||
}
|
||||
|
||||
public function testAttachmentCacheInRuntimeStorage() {
|
||||
$cacheStorage = new VolatileRuntimeStorage;
|
||||
$logger = new InMemoryLogger;
|
||||
|
||||
$coid = new IRI('coid://cloudobjects.io/Test');
|
||||
$cacheId = (string)$coid.'#file.txt';
|
||||
|
||||
$retriever = new ObjectRetriever([
|
||||
'cache_storage' => $cacheStorage,
|
||||
'logger' => $logger
|
||||
]);
|
||||
|
||||
$content1 = $retriever->getAttachment($coid, 'file.txt');
|
||||
|
||||
$this->assertEquals('Test file content', trim($content1));
|
||||
$this->assertNotNull($cacheStorage->fetch($retriever->getCacheKey($cacheId)));
|
||||
$this->assertStringContainsString('from Core API', $logger->getLastLogMessage());
|
||||
|
||||
// Reinitialize retriever with same cache storage to verify that cache is used
|
||||
$retriever = new ObjectRetriever([
|
||||
'cache_storage' => $cacheStorage,
|
||||
'logger' => $logger
|
||||
]);
|
||||
|
||||
$content2 = $retriever->getAttachment($coid, 'file.txt');
|
||||
|
||||
$this->assertEquals('Test file content', trim($content2));
|
||||
$this->assertStringContainsString('from object cache', $logger->getLastLogMessage());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -33,6 +33,15 @@ class ObjectRetrieverPublicTest extends \PHPUnit\Framework\TestCase {
|
||||
$this->assertEquals('CloudObjects', $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue());
|
||||
}
|
||||
|
||||
public function testGetTestObject() {
|
||||
$coid = new IRI('coid://cloudobjects.io/Test');
|
||||
$object = $this->retriever->getObjectNode($coid);
|
||||
$this->assertNotNull($object);
|
||||
$this->assertEquals((string)$coid, $object->getId());
|
||||
$this->assertNotNull($object->getProperty('http://www.w3.org/2000/01/rdf-schema#label'));
|
||||
$this->assertEquals('Test Object', $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue());
|
||||
}
|
||||
|
||||
public function testGetRelatedObject() {
|
||||
$coid = new IRI('coid://cloudobjects.io');
|
||||
$object = $this->retriever->getCloudObject($coid);
|
||||
|
||||
Reference in new issue
Block a user