Compare commits

19 Commits

Author SHA1 Message Date
ce77709d37 Added global function cloudobjects_get_object() 2026-06-15 09:29:04 +00:00
a7eb83be44 Implemented a facade for ObjectRetriever to allow static access 2026-06-15 09:19:59 +00:00
3653513818 Simplified comment 2026-06-15 09:06:53 +00:00
58df9cb476 Added getLabel() helper function 2026-06-15 09:04:16 +00:00
3c958dc5df Throw exception when namespace cannot be retrieved 2026-06-12 18:28:30 +00:00
993218f21d getAuthenticatingNamespaceObject* may return null as well 2026-06-12 17:52:55 +00:00
aa37259a21 Extend SDKLoader to support non-OO SDKs, starting with Sentry 2026-06-12 17:38:13 +00:00
69a16e64d1 Fixed return type hints to not fail when objects are null 2026-06-12 17:30:25 +00:00
cb03c811ef Allow deeper nesting of hostnames 2026-06-02 13:19:25 +00:00
6f3c7338c7 Changed indentation and added type hints 2026-06-02 13:14:16 +00:00
da0ddd572e Refactored and renamed getAuthenticatingNamespaceObject(), added more type hints 2026-06-02 12:32:09 +00:00
5f5a31df68 Improved cache reading 2026-06-01 17:45:55 +00:00
58585a6875 Fixed copy&paste error in README 2026-05-26 15:39:03 +00:00
23f00b2374 Remove outdated doctrine/cache package and change configuration 2026-05-26 15:38:15 +00:00
9f339953fe Fixed Buddy badge 2026-05-15 14:46:22 +00:00
d5d766cbf4 Added getRevision() helper function, refactored revision constant 2026-05-15 14:06:15 +00:00
06df8b0be1 Use new API endpoint URL 2026-05-15 11:24:21 +00:00
de355ff2f9 Added a has() method to CloudObject 2026-05-15 11:22:15 +00:00
7d8ece0df1 Set visibility of getReader to allow inheritance 2026-05-15 11:20:41 +00:00
19 changed files with 923 additions and 778 deletions

View File

@@ -13,140 +13,140 @@ use ML\IRI\IRI;
*/ */
class COIDParser { class COIDParser {
const COID_INVALID = 0; const COID_INVALID = 0;
const COID_ROOT = 1; const COID_ROOT = 1;
const COID_UNVERSIONED = 2; const COID_UNVERSIONED = 2;
const COID_VERSIONED = 3; const COID_VERSIONED = 3;
const COID_VERSION_WILDCARD = 4; const COID_VERSION_WILDCARD = 4;
const REGEX_HOSTNAME = "/^([a-z0-9-]+\.)?[a-z0-9-]+\.[a-z]+$/"; const REGEX_HOSTNAME = "/^([a-z0-9-]+\.)*[a-z0-9-]+\.[a-z]+$/";
const REGEX_SEGMENT = "/^[A-Za-z-_0-9\.]+$/"; const REGEX_SEGMENT = "/^[A-Za-z-_0-9\.]+$/";
const REGEX_VERSION_WILDCARD = "/^((\^|~)(\d+\.)?\d|(\d+\.){1,2}\*)$/"; const REGEX_VERSION_WILDCARD = "/^((\^|~)(\d+\.)?\d|(\d+\.){1,2}\*)$/";
/** /**
* Creates a new IRI object representing a COID from a string. * Creates a new IRI object representing a COID from a string.
* Adds the "coid://" prefix if necessary and normalizes case. * Adds the "coid://" prefix if necessary and normalizes case.
* *
* @param string $coidString A COID string. * @param string $coidString A COID string.
* @return IRI * @return IRI
*/ */
public static function fromString($coidString) { public static function fromString($coidString) : IRI {
$coidPre = new IRI( $coidPre = new IRI(
(strtolower(substr($coidString, 0, 7))=='coid://') ? $coidString : 'coid://'.$coidString (strtolower(substr($coidString, 0, 7)) == 'coid://') ? $coidString : 'coid://'.$coidString
); );
// Normalize scheme and host segments to lower case // Normalize scheme and host segments to lower case
return new IRI('coid://'.strtolower($coidPre->getHost()).$coidPre->getPath()); return new IRI('coid://'.strtolower($coidPre->getHost()).$coidPre->getPath());
}
/**
* Get the type of a COID.
*
* @param IRI $coid
* @return int|null
*/
public static function getType(IRI $coid) {
if ($coid->getScheme()!='coid' || $coid->getHost()==''
|| preg_match(self::REGEX_HOSTNAME, $coid->getHost()) != 1)
return self::COID_INVALID;
if ($coid->getPath()=='' || $coid->getPath()=='/')
return self::COID_ROOT;
$segments = explode('/', $coid->getPath());
switch (count($segments)) {
case 2:
return (preg_match(self::REGEX_SEGMENT, $segments[1]) == 1)
? self::COID_UNVERSIONED
: self::COID_INVALID;
case 3:
if (preg_match(self::REGEX_SEGMENT, $segments[1]) != 1)
return self::COID_INVALID;
if (preg_match(self::REGEX_SEGMENT, $segments[2]) == 1)
return self::COID_VERSIONED;
else
if (preg_match(self::REGEX_VERSION_WILDCARD, $segments[2]) == 1)
return self::COID_VERSION_WILDCARD;
else
return self::COID_INVALID;
default:
return self::COID_INVALID;
} }
}
/** /**
* Checks whether the given IRI object is a valid COID. * Get the type of a COID.
* *
* @param IRI $coid * @param IRI $coid
* @return boolean * @return int|null
*/ */
public static function isValidCOID(IRI $coid) { public static function getType(IRI $coid) : ?int {
return (self::getType($coid)!=self::COID_INVALID); if ($coid->getScheme()!='coid' || $coid->getHost()==''
} || preg_match(self::REGEX_HOSTNAME, $coid->getHost()) != 1)
return self::COID_INVALID;
/** if ($coid->getPath()=='' || $coid->getPath()=='/')
* Get the name segment of a valid COID or null if not available. return self::COID_ROOT;
*
* @param IRI $coid
* @return string|null
*/
public static function getName(IRI $coid) {
if (self::getType($coid)!=self::COID_INVALID
&& self::getType($coid)!=self::COID_ROOT) {
$segments = explode('/', $coid->getPath());
return $segments[1];
} else
return null;
}
/** $segments = explode('/', $coid->getPath());
* Get the version segment of a valid, versioned COID or null if not available. switch (count($segments)) {
* case 2:
* @param IRI $coid return (preg_match(self::REGEX_SEGMENT, $segments[1]) == 1)
* @return string|null ? self::COID_UNVERSIONED
*/ : self::COID_INVALID;
public static function getVersion(IRI $coid) { case 3:
if (self::getType($coid)==self::COID_VERSIONED) { if (preg_match(self::REGEX_SEGMENT, $segments[1]) != 1)
$segments = explode('/', $coid->getPath()); return self::COID_INVALID;
return $segments[2];
} else
return null;
}
/** if (preg_match(self::REGEX_SEGMENT, $segments[2]) == 1)
* Get the version segment of a versioned or version wildcard COID or return self::COID_VERSIONED;
* null if not available. else
* if (preg_match(self::REGEX_VERSION_WILDCARD, $segments[2]) == 1)
* @param IRI $coid return self::COID_VERSION_WILDCARD;
* @return string|null else
*/ return self::COID_INVALID;
public static function getVersionWildcard(IRI $coid) { default:
if (self::getType($coid)==self::COID_VERSION_WILDCARD) { return self::COID_INVALID;
$segments = explode('/', $coid->getPath()); }
return $segments[2]; }
} else
return null; /**
} * Checks whether the given IRI object is a valid COID.
*
/** * @param IRI $coid
* Returns the COID itself if it is a root COID or a new IRI object * @return bool
* representing the namespace underlying the given COID. */
* public static function isValidCOID(IRI $coid) : bool {
* @param IRI $coid return (self::getType($coid)!=self::COID_INVALID);
* @return IRI|null }
*/
public static function getNamespaceCOID(IRI $coid) { /**
switch (self::getType($coid)) { * Get the name segment of a valid COID or null if not available.
case self::COID_ROOT: *
return $coid; * @param IRI $coid
case self::COID_UNVERSIONED: * @return string|null
case self::COID_VERSIONED: */
case self::COID_VERSION_WILDCARD: public static function getName(IRI $coid) : ?string {
return new IRI('coid://'.$coid->getHost()); if (self::getType($coid)!=self::COID_INVALID
default: && self::getType($coid)!=self::COID_ROOT) {
return null; $segments = explode('/', $coid->getPath());
return $segments[1];
} else
return null;
}
/**
* Get the version segment of a valid, versioned COID or null if not available.
*
* @param IRI $coid
* @return string|null
*/
public static function getVersion(IRI $coid) : ?string {
if (self::getType($coid)==self::COID_VERSIONED) {
$segments = explode('/', $coid->getPath());
return $segments[2];
} else
return null;
}
/**
* Get the version segment of a versioned or version wildcard COID or
* null if not available.
*
* @param IRI $coid
* @return string|null
*/
public static function getVersionWildcard(IRI $coid) : ?string {
if (self::getType($coid)==self::COID_VERSION_WILDCARD) {
$segments = explode('/', $coid->getPath());
return $segments[2];
} else
return null;
}
/**
* Returns the COID itself if it is a root COID or a new IRI object
* representing the namespace underlying the given COID.
*
* @param IRI $coid
* @return IRI|null
*/
public static function getNamespaceCOID(IRI $coid) : ?IRI {
switch (self::getType($coid)) {
case self::COID_ROOT:
return $coid;
case self::COID_UNVERSIONED:
case self::COID_VERSIONED:
case self::COID_VERSION_WILDCARD:
return new IRI('coid://'.$coid->getHost());
default:
return null;
}
} }
}
} }

View File

@@ -39,7 +39,11 @@ class CloudObject {
return $this; return $this;
} }
private function getReader() : NodeReader { /**
* Get the NodeReader used for this CloudObject.
* Returns a default NodeReader if non has been set.
*/
protected function getReader() : NodeReader {
if (!$this->reader) { if (!$this->reader) {
$this->reader = new NodeReader; $this->reader = new NodeReader;
} }
@@ -70,6 +74,13 @@ class CloudObject {
return $this->node; return $this->node;
} }
/**
* Checks if a property exists on this object.
*/
public function has($property) : bool {
return $this->getReader()->hasProperty($this->node, $property);
}
/** /**
* Get the value of a property as a string. * Get the value of a property as a string.
* If the property has multiple values, only the first is returned. * If the property has multiple values, only the first is returned.
@@ -133,7 +144,6 @@ class CloudObject {
return $this->retriever->getCloudObject($coid); return $this->retriever->getCloudObject($coid);
} }
/** /**
* Get the value of a property and, if it's a COID, retrieve the corresponding object node. * Get the value of a property and, if it's a COID, retrieve the corresponding object node.
*/ */
@@ -147,6 +157,20 @@ class CloudObject {
} }
return $this->retriever->getObjectNode($coid); return $this->retriever->getObjectNode($coid);
} }
/**
* Get the revision of the object.
*/
public function getRevision() : string {
return $this->getString(Constants::PROPERTY_REVISION);
}
/**
* Get the label of the object.
*/
public function getLabel() : ?string {
return $this->getString(Constants::RDFS_LABEL);
}
} }

View File

@@ -60,7 +60,7 @@ class CryptoHelper {
$this->objectRetriever = $objectRetriever; $this->objectRetriever = $objectRetriever;
$this->namespace = isset($namespaceCoid) $this->namespace = isset($namespaceCoid)
? $objectRetriever->getObjectNode($namespaceCoid) ? $objectRetriever->getObjectNode($namespaceCoid)
: $objectRetriever->getAuthenticatingNamespaceObject(); : $objectRetriever->getAuthenticatingNamespaceObjectNode();
$this->reader = new NodeReader([ $this->reader = new NodeReader([
'prefixes' => [ 'prefixes' => [

View File

@@ -0,0 +1,14 @@
<?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;
class Constants {
const PROPERTY_REVISION = 'coid://cloudobjects.io/isAtRevision';
const RDFS_LABEL = 'http://www.w3.org/2000/01/rdf-schema#label';
}

View File

@@ -0,0 +1,14 @@
<?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\Exceptions;
/**
* An Exception that is thrown when the SDK's configuration isn't valid.
*/
class InvalidSDKConfigurationException extends \Exception {
}

View File

@@ -7,6 +7,7 @@
namespace CloudObjects\SDK\Helpers; namespace CloudObjects\SDK\Helpers;
use Exception; use Exception;
use CloudObjects\SDK\Exceptions\InvalidSDKConfigurationException;
use CloudObjects\SDK\NodeReader, CloudObjects\SDK\ObjectRetriever; use CloudObjects\SDK\NodeReader, CloudObjects\SDK\ObjectRetriever;
/** /**
@@ -27,20 +28,53 @@ class SDKLoader {
$this->reader = new NodeReader; $this->reader = new NodeReader;
} }
/**
* Initialize the SDK with the given name and options; used for
* SDKs that are initialized with a function and not a class name.
*
* @param string $sdkName The name of the SDK to initialize.
* @param array $options Additional options for the SDK (if necessary).
* @return mixed The return value of the SDK initialization, which may vary depending on the SDK.
* @throws Exception If the SDK is not supported or cannot be initialized.
*/
public function init(string $sdkName, array $options = []) : mixed {
$namespace = $this->objectRetriever->getAuthenticatingNamespaceCloudObject();
if (!$namespace)
throw new InvalidSDKConfigurationException("The authenticating namespace object could not be retrieved.");
switch (strtolower($sdkName)) {
case "sentry":
// --- Sentry (https://sentry.io/) ---
$initFunction = '\Sentry\init';
return $initFunction(array_merge([
'dsn' => $namespace->getString('coid://sentry.io.3rd-party.co/DSN')
], $options));
default:
throw new Exception("No rules defined to initialize SDK with name <".$sdkName.">.");
}
}
/** /**
* Initialize and return the SDK with the given classname. * Initialize and return the SDK with the given classname.
* Throws Exception if the SDK is not supported. * Throws Exception if the SDK is not supported.
* *
* @param $classname Classname for the SDK's main class * @param string $classname Classname for the SDK's main class.
* @param array $options Additional options for the SDK (if necessary) * @param array $options Additional options for the SDK (if necessary).
* @return mixed The initialized SDK instance.
* @throws Exception If the SDK is not supported or cannot be initialized.
*/ */
public function get($classname, array $options) { public function get(string $classname, array $options) : mixed {
if (!class_exists($classname)) if (!class_exists($classname))
throw new Exception("<".$classname."> is not a valid classname."); throw new Exception("<".$classname."> is not a valid classname.");
$hashkey = md5($classname.serialize($options)); $hashkey = md5($classname.serialize($options));
if (!isset($this->classes[$hashkey])) { if (!isset($this->classes[$hashkey])) {
$nsNode = $this->objectRetriever->getAuthenticatingNamespaceObject(); $nsNode = $this->objectRetriever->getAuthenticatingNamespaceObjectNode();
if (!$nsNode)
throw new InvalidSDKConfigurationException("The authenticating namespace object could not be retrieved.");
// --- Amazon Web Services (https://aws.amazon.com/) --- // --- Amazon Web Services (https://aws.amazon.com/) ---
// has multiple classnames, so check for common superclass // has multiple classnames, so check for common superclass

View File

@@ -6,15 +6,24 @@
namespace CloudObjects\SDK; namespace CloudObjects\SDK;
use Exception; use DateTime, Exception;
use ML\IRI\IRI, ML\JsonLD\JsonLD; use ML\IRI\IRI;
use Doctrine\Common\Cache\RedisCache; use ML\JsonLD\JsonLD, ML\JsonLD\Node;
use Psr\Log\LoggerInterface, Psr\Log\LoggerAwareTrait; use Psr\Log\LoggerInterface, Psr\Log\LoggerAwareTrait;
use GuzzleHttp\ClientInterface, GuzzleHttp\Client, GuzzleHttp\HandlerStack; use GuzzleHttp\ClientInterface, GuzzleHttp\Client, GuzzleHttp\HandlerStack;
use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\RequestException;
use Kevinrob\GuzzleCache\CacheMiddleware, Kevinrob\GuzzleCache\Storage\DoctrineCacheStorage; use GuzzleHttp\Psr7\Request,
GuzzleHttp\Psr7\Response;
use Kevinrob\GuzzleCache\CacheMiddleware;
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy; use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
use CloudObjects\SDK\Exceptions\CoreAPIException; use Kevinrob\GuzzleCache\Storage\CacheStorageInterface,
Kevinrob\GuzzleCache\Storage\Psr6CacheStorage,
Kevinrob\GuzzleCache\Storage\Psr16CacheStorage,
Kevinrob\GuzzleCache\CacheEntry;
use Psr\Cache\CacheItemPoolInterface;
use Psr\SimpleCache\CacheInterface;
use CloudObjects\SDK\Exceptions\CoreAPIException,
CloudObjects\SDK\Exceptions\InvalidSDKConfigurationException;
use CloudObjects\SDK\AccountGateway\AccountContext; use CloudObjects\SDK\AccountGateway\AccountContext;
/** /**
@@ -30,15 +39,14 @@ class ObjectRetriever implements CustomCacheAndLogInterface {
private $cache; private $cache;
private $objects; private $objects;
private $defaultReader; private $defaultReader;
private $customCacheDefaultRequest;
const CO_API_URL = 'https://api.cloudobjects.net/'; const CO_API_URL = 'https://od.coid.link/';
const REVISION_PROPERTY = 'coid://cloudobjects.io/isAtRevision';
public function __construct($options = []) { public function __construct($options = []) {
// Merge options with defaults // Merge options with defaults
$this->options = array_merge([ $this->options = array_merge([
'cache_provider' => 'none', 'cache_storage' => null,
'cache_prefix' => 'clobj:', 'cache_prefix' => 'clobj:',
'cache_ttl' => 60, 'cache_ttl' => 60,
'static_config_path' => null, 'static_config_path' => null,
@@ -50,35 +58,27 @@ class ObjectRetriever implements CustomCacheAndLogInterface {
'connect_timeout' => 5 'connect_timeout' => 5
], $options); ], $options);
// Set up object cache // Check object cache configuration
switch ($this->options['cache_provider']) { if (isset($this->options['cache_storage'])
case 'none': && !($this->options['cache_storage'] instanceof CacheStorageInterface)
// no caching && !($this->options['cache_storage'] instanceof CacheItemPoolInterface)
$this->cache = null; && !($this->options['cache_storage'] instanceof CacheInterface)
break; ) {
case 'redis': throw new InvalidSDKConfigurationException('Invalid cache_storage specified; must be an instance of Kevinrob\GuzzleCache\CacheStorageInterface, Psr\Cache\CacheItemPoolInterface or Psr\SimpleCache\CacheInterface.');
// caching with Redis }
$redis = new \Redis();
$redis->pconnect(
isset($this->options['cache_provider.redis.host']) ? $this->options['cache_provider.redis.host'] : '127.0.0.1',
isset($this->options['cache_provider.redis.port']) ? $this->options['cache_provider.redis.port'] : 6379);
$this->cache = new RedisCache(); if ($this->options['cache_storage'] instanceof CacheStorageInterface) {
$this->cache->setRedis($redis); $this->cache = $this->options['cache_storage'];
break; } elseif ($this->options['cache_storage'] instanceof CacheItemPoolInterface) {
case 'file': $this->cache = new Psr6CacheStorage($this->options['cache_storage']);
// caching on the filesystem } elseif ($this->options['cache_storage'] instanceof CacheInterface) {
$this->cache = new \Doctrine\Common\Cache\FilesystemCache( $this->cache = new Psr16CacheStorage($this->options['cache_storage']);
isset($this->options['cache_provider.file.directory']) ? $this->options['cache_provider.file.directory'] : sys_get_temp_dir()
);
break;
default:
throw new Exception('Valid values for cache_provider are: none, redis, file');
} }
// Set up logger // Set up logger
if (is_a($this->options['logger'], LoggerInterface::class)) if (is_a($this->options['logger'], LoggerInterface::class)) {
$this->setLogger($this->options['logger']); $this->setLogger($this->options['logger']);
}
// Set up handler stack // Set up handler stack
$stack = HandlerStack::create(); $stack = HandlerStack::create();
@@ -87,11 +87,14 @@ class ObjectRetriever implements CustomCacheAndLogInterface {
if (isset($this->cache)) { if (isset($this->cache)) {
$stack->push( $stack->push(
new CacheMiddleware( new CacheMiddleware(
new PrivateCacheStrategy( new PrivateCacheStrategy($this->cache)
new DoctrineCacheStorage($this->cache)
)
) )
); );
// We also use the cache for storing object descriptions and attachments
// without the HTTP request, so we create a dummy request; this way
// we can reuse GuzzleCache for maximum compatibility
$this->customCacheDefaultRequest = new Request('GET', '/');
} }
// Initialize client // Initialize client
@@ -129,18 +132,32 @@ class ObjectRetriever implements CustomCacheAndLogInterface {
$this->logger->info($message, [ 'elapsed_ms' => round((microtime(true) - $ts) * 1000) ]); $this->logger->info($message, [ 'elapsed_ms' => round((microtime(true) - $ts) * 1000) ]);
} }
private function getCacheKey($id) { public function getCacheKey($id) {
return $this->options['cache_prefix'].$this->options['auth_ns'].'/'.$id; return $this->options['cache_prefix'] . $this->options['auth_ns'] . '/' . $id;
} }
private function getFromCache($id) { private function getFromCache($id) {
return (isset($this->cache) && $this->cache->contains($this->getCacheKey($id))) if (!$this->cache) {
? $this->cache->fetch($this->getCacheKey($id)) : null; return null;
}
$cachedData = $this->cache->fetch($this->getCacheKey($id));
if (!$cachedData) {
return null;
}
return (string)$cachedData->getResponse()->getBody(true);
} }
private function putIntoCache($id, $data, $ttl) { private function putIntoCache($id, $data, $ttl) {
if (isset($this->cache)) if ($this->cache) {
$this->cache->save($this->getCacheKey($id), $data, $ttl); $entry = new CacheEntry($this->customCacheDefaultRequest,
new Response(200, [], $data),
((new DateTime)->modify('+'.$ttl.' seconds')));
$this->cache->save($this->getCacheKey($id), $entry, $ttl);
}
} }
public function getFromCacheCustom($id) { public function getFromCacheCustom($id) {
@@ -189,7 +206,7 @@ class ObjectRetriever implements CustomCacheAndLogInterface {
/** /**
* Get an object description and return a CloudObject. * Get an object description and return a CloudObject.
*/ */
public function getCloudObject(IRI $coid) { public function getCloudObject(IRI $coid) : ?CloudObject {
$node = $this->getObjectNode($coid); $node = $this->getObjectNode($coid);
if (!$node) { if (!$node) {
// Object not found // Object not found
@@ -222,7 +239,7 @@ class ObjectRetriever implements CustomCacheAndLogInterface {
* @param IRI $coid COID of the object * @param IRI $coid COID of the object
* @return Node|null * @return Node|null
*/ */
public function getObjectNode(IRI $coid) { public function getObjectNode(IRI $coid) : ?Node {
if (!COIDParser::isValidCOID($coid)) if (!COIDParser::isValidCOID($coid))
throw new Exception("Not a valid COID."); throw new Exception("Not a valid COID.");
@@ -479,7 +496,7 @@ class ObjectRetriever implements CustomCacheAndLogInterface {
} }
if (!isset($fileData) if (!isset($fileData)
|| $fileRevision!=$object->getProperty(self::REVISION_PROPERTY)->getValue()) { || $fileRevision !== $object->getProperty(Constants::PROPERTY_REVISION)->getValue()) {
// Does not exist in cache or is outdated, fetch from CloudObjects // Does not exist in cache or is outdated, fetch from CloudObjects
try { try {
@@ -487,7 +504,7 @@ class ObjectRetriever implements CustomCacheAndLogInterface {
.'/'.basename($filename)); .'/'.basename($filename));
$fileContent = $response->getBody()->getContents(); $fileContent = $response->getBody()->getContents();
$fileData = $object->getProperty(self::REVISION_PROPERTY)->getValue().'#'.$fileContent; $fileData = $object->getProperty(Constants::PROPERTY_REVISION)->getValue().'#'.$fileContent;
$this->putIntoCache($cacheId, $fileData, 0); $this->putIntoCache($cacheId, $fileData, 0);
$this->logInfoWithTime('Fetched attachment <'.$filename.'> for <'.$object->getId().'> from Core API ['.$response->getStatusCode().'].', $ts); $this->logInfoWithTime('Fetched attachment <'.$filename.'> for <'.$object->getId().'> from Core API ['.$response->getStatusCode().'].', $ts);
@@ -501,21 +518,46 @@ class ObjectRetriever implements CustomCacheAndLogInterface {
return $fileContent; return $fileContent;
} }
/** private function assertAuthenticatingNamespaceAndGetId() : IRI {
* Retrieve the object that describes the namespace provided with the "auth_ns"
* configuration option.
*
* @return Node
*/
public function getAuthenticatingNamespaceObject() {
if (!isset($this->options['auth_ns'])) if (!isset($this->options['auth_ns']))
throw new Exception("Missing 'auth_ns' configuration option."); throw new InvalidSDKConfigurationException("Missing 'auth_ns' configuration option.");
$namespaceCoid = COIDParser::fromString($this->options['auth_ns']); $namespaceCoid = COIDParser::fromString($this->options['auth_ns']);
if (COIDParser::getType($namespaceCoid) != COIDParser::COID_ROOT) if (COIDParser::getType($namespaceCoid) != COIDParser::COID_ROOT)
throw new Exception("The 'auth_ns' configuration option is not a valid namespace/root COID."); throw new InvalidSDKConfigurationException("The 'auth_ns' configuration option is not a valid namespace/root COID.");
return $this->getObject($namespaceCoid); return $namespaceCoid;
}
/**
* Retrieve the object node that describes the namespace
* provided with the "auth_ns" configuration option.
*
* @deprecated Use getAuthenticatingNamespaceObjectNode() instead
* @return Node
*/
public function getAuthenticatingNamespaceObject() : ?Node {
return $this->getObject($this->assertAuthenticatingNamespaceAndGetId());
}
/**
* Retrieve the object node that describes the namespace
* provided with the "auth_ns" configuration option.
*
* @return Node
*/
public function getAuthenticatingNamespaceObjectNode() : ?Node {
return $this->getObject($this->assertAuthenticatingNamespaceAndGetId());
}
/**
* Retrieve the CloudObject that describes the namespace
* provided with the "auth_ns" configuration option.
*
* @return CloudObject
*/
public function getAuthenticatingNamespaceCloudObject() : ?CloudObject {
return $this->getCloudObject($this->assertAuthenticatingNamespaceAndGetId());
} }
} }

View File

@@ -0,0 +1,80 @@
<?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 Exception;
use ML\IRI\IRI;
use ML\JsonLD\Node;
/**
* Static facade for ObjectRetriever. Call setObjectRetriever() once to
* initialize; all subsequent static calls are forwarded to that instance.
*/
class ObjectRetrieverFacade {
private static ?ObjectRetriever $instance = null;
public static function setObjectRetriever(ObjectRetriever $retriever) : void {
self::$instance = $retriever;
}
private static function getInstance() : ObjectRetriever {
if (self::$instance === null)
throw new Exception('ObjectRetrieverFacade has not been initialized. Call setObjectRetriever() first.');
return self::$instance;
}
public static function setDefaultReader(NodeReader $reader) : ObjectRetriever {
return self::getInstance()->setDefaultReader($reader);
}
public static function getDefaultReader() : NodeReader {
return self::getInstance()->getDefaultReader();
}
public static function getClient() {
return self::getInstance()->getClient();
}
public static function getCloudObject(IRI $coid) : ?CloudObject {
return self::getInstance()->getCloudObject($coid);
}
public static function getObjectNode(IRI $coid) : ?Node {
return self::getInstance()->getObjectNode($coid);
}
public static function fetchObjectsInNamespaceWithType(IRI $namespaceCoid, $type) : array {
return self::getInstance()->fetchObjectsInNamespaceWithType($namespaceCoid, $type);
}
public static function fetchAllObjectsInNamespace(IRI $namespaceCoid) : array {
return self::getInstance()->fetchAllObjectsInNamespace($namespaceCoid);
}
public static function getCOIDListForNamespace(IRI $namespaceCoid) : array {
return self::getInstance()->getCOIDListForNamespace($namespaceCoid);
}
public static function getCOIDListForNamespaceWithType(IRI $namespaceCoid, $type) : array {
return self::getInstance()->getCOIDListForNamespaceWithType($namespaceCoid, $type);
}
public static function getAttachment(IRI $coid, $filename) {
return self::getInstance()->getAttachment($coid, $filename);
}
public static function getAuthenticatingNamespaceObjectNode() : ?Node {
return self::getInstance()->getAuthenticatingNamespaceObjectNode();
}
public static function getAuthenticatingNamespaceCloudObject() : ?CloudObject {
return self::getInstance()->getAuthenticatingNamespaceCloudObject();
}
}

View File

@@ -0,0 +1,63 @@
<?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\TestHelpers;
use Psr\Log\LoggerInterface;
class InMemoryLogger implements LoggerInterface {
private $logs = [];
public function getLogs() : array {
return $this->logs;
}
public function getLastLogMessage() : string {
if (empty($this->logs)) {
return '';
}
return end($this->logs)['message'];
}
public function emergency(string|\Stringable $message, array $context = []): void {
$this->logs[] = [ 'level' => 'emergency', 'message' => (string)$message, 'context' => $context ];
}
public function alert(string|\Stringable $message, array $context = []): void {
$this->logs[] = [ 'level' => 'alert', 'message' => (string)$message, 'context' => $context ];
}
public function critical(string|\Stringable $message, array $context = []): void {
$this->logs[] = [ 'level' => 'critical', 'message' => (string)$message, 'context' => $context ];
}
public function error(string|\Stringable $message, array $context = []): void {
$this->logs[] = [ 'level' => 'error', 'message' => (string)$message, 'context' => $context ];
}
public function warning(string|\Stringable $message, array $context = []): void {
$this->logs[] = [ 'level' => 'warning', 'message' => (string)$message, 'context' => $context ];
}
public function notice(string|\Stringable $message, array $context = []): void {
$this->logs[] = [ 'level' => 'notice', 'message' => (string)$message, 'context' => $context ];
}
public function info(string|\Stringable $message, array $context = []): void {
$this->logs[] = [ 'level' => 'info', 'message' => (string)$message, 'context' => $context ];
}
public function debug(string|\Stringable $message, array $context = []): void {
$this->logs[] = [ 'level' => 'debug', 'message' => (string)$message, 'context' => $context ];
}
public function log($level, string|\Stringable $message, array $context = []): void {
$this->logs[] = [ 'level' => $level, 'message' => (string)$message, 'context' => $context ];
}
}

View File

@@ -221,7 +221,7 @@ class APIClientFactory {
$this->objectRetriever = $objectRetriever; $this->objectRetriever = $objectRetriever;
$this->namespace = isset($namespaceCoid) $this->namespace = isset($namespaceCoid)
? $objectRetriever->getObjectNode($namespaceCoid) ? $objectRetriever->getObjectNode($namespaceCoid)
: $objectRetriever->getAuthenticatingNamespaceObject(); : $objectRetriever->getAuthenticatingNamespaceObjectNode();
$this->reader = new NodeReader([ $this->reader = new NodeReader([
'prefixes' => [ 'prefixes' => [

View File

@@ -0,0 +1,23 @@
<?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/. */
use ML\IRI\IRI;
use CloudObjects\SDK\CloudObject,
CloudObjects\SDK\ObjectRetrieverFacade;
if (!function_exists('cloudobjects_get_object')) {
/**
* Retrieve a CloudObject by COID. Accepts a COID as an IRI object or a string.
*/
function cloudobjects_get_object($coid) : ?CloudObject {
if (is_string($coid))
$coid = new IRI($coid);
elseif (!($coid instanceof IRI))
throw new InvalidArgumentException('COID must be a string or an IRI object.');
return ObjectRetrieverFacade::getCloudObject($coid);
}
}

View File

@@ -2,67 +2,18 @@
[![Latest Stable Version](https://poser.pugx.org/cloudobjects/sdk/v/stable)](https://packagist.org/packages/cloudobjects/sdk) [![Total Downloads](https://poser.pugx.org/cloudobjects/sdk/downloads)](https://packagist.org/packages/cloudobjects/sdk) [![Latest Stable Version](https://poser.pugx.org/cloudobjects/sdk/v/stable)](https://packagist.org/packages/cloudobjects/sdk) [![Total Downloads](https://poser.pugx.org/cloudobjects/sdk/downloads)](https://packagist.org/packages/cloudobjects/sdk)
[![buddy branch](https://app.buddy.works/cloudobjects/php-sdk/repository/branch/main/badge.svg?token=52ae28bf71dbbd3dde018f3f3e7caafa04f95bdb451b3dfca547414ec7a01739 "buddy branch")](https://app.buddy.works/cloudobjects/php-sdk/repository/branch/main) [![buddy pipeline](https://app.buddy.works/cloudobjects/cloudobjects-php-sdk/pipelines/pipeline/561203/badge.svg?token=40c63ec4ea9e432a9edc8ebfb0ba0e203f70a02a8208339b119ff16772f4abd4 "buddy pipeline")](https://app.buddy.works/cloudobjects/cloudobjects-php-sdk/pipelines/pipeline/561203)
The CloudObjects PHP SDK provides simple access to [CloudObjects](https://cloudobjects.io/) from PHP-based applications. It wraps the [Object API](https://coid.link/cloudobjects.io/ObjectAPI/1.0) to fetch objects from the CloudObjects Core database and provides object-based access to their RDF description. A two-tiered caching mechanism (in-memory and Doctrine cache drivers) is included. The SDK also contains a helper class to validate COIDs.
## Installation ## Installation
The SDK is [distributed through packagist](https://packagist.org/packages/cloudobjects/sdk). Add `cloudobjects/sdk` to the `require` section of your `composer.json`, like this: The SDK is [distributed through packagist](https://packagist.org/packages/cloudobjects/sdk).
````json Install with the following command:
{
"require": {
"cloudobjects/sdk" : ">=0.7"
}
}
````
## Retrieving Objects ```
composer require cloudobjects/sdk
In order to retrieve objects from the CloudObjects Core database you need to create an instance of `CloudObjects\SDK\ObjectRetriever`. Then you can call `getObject()`. This method returns an `ML\JsonLD\Node` instance or `null` if the object is not found. You can use the object interface of the [JsonLD library](https://github.com/lanthaler/JsonLD/) to read the information from the object. ```
Here's a simple example:
````php
use ML\IRI\IRI;
use CloudObjects\SDK\ObjectRetriever;
/* ... */
$retriever = new ObjectRetriever();
$object = $this->retriever->getObject(new IRI('coid://cloudobjects.io'));
if (isset($object))
echo $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue();
else
echo "Object not found.";
````
### Configuration
You can pass an array of configuration options to the ObjectRetriever's constructor:
| Option | Description | Default |
|---|---|---|
| `cache_provider` | The type of cache used. Currently supports `redis`, `file` and `none`. | `none` |
| `cache_prefix` | A prefix used for cache IDs. Normally this should not be set but might be necessary on shared caches. | `clobj:` |
| `cache_ttl` | Determines how long objects can remain cached. | `60` |
| `auth_ns` | The namespace of the service that this retriever acts for. If not set the API is accessed anonymously. | `null` |
| `auth_secret` | The shared secret between the namespace in `auth_ns` and `cloudobjects.io` for authenticated. If not set the API is accessed anonymously. | `null` |
#### For `redis` cache:
| Option | Description | Default |
|---|---|---|
| `cache_provider.redis.host` | The hostname or IP of the Redis instance. | `127.0.0.1` |
| `cache_provider.redis.port` | The port number of the Redis instance. | `6379` |
#### For `file` cache:
| Option | Description | Default |
|---|---|---|
| `cache_provider.file.directory` | The directory to store cache data in. | The system's temporary directory. |
## License ## License
The PHP SDK is licensed under Mozilla Public License (see LICENSE file). The PHP SDK is licensed under Mozilla Public License (see LICENSE file).

View File

@@ -6,12 +6,12 @@
"license": "MPL-2.0", "license": "MPL-2.0",
"require" : { "require" : {
"ml/json-ld": ">=1.0.7", "ml/json-ld": ">=1.0.7",
"doctrine/common" : ">=2.6.1",
"doctrine/cache" : "1.*",
"guzzlehttp/guzzle" : ">=6.0", "guzzlehttp/guzzle" : ">=6.0",
"psr/cache": ">=1.0",
"psr/log": ">=1.1", "psr/log": ">=1.1",
"kevinrob/guzzle-cache-middleware": "^7.0.0", "kevinrob/guzzle-cache-middleware": "^7.0.0",
"webmozart/assert": "^1.6" "webmozart/assert": "^1.6",
"psr/simple-cache": "^3.0"
}, },
"authors": [ "authors": [
{ {
@@ -21,7 +21,10 @@
"autoload": { "autoload": {
"psr-0": { "psr-0": {
"CloudObjects\\SDK" : "" "CloudObjects\\SDK" : ""
} },
"files": [
"CloudObjects/SDK/functions.php"
]
}, },
"require-dev" : { "require-dev" : {
"phpunit/phpunit": "^10", "phpunit/phpunit": "^10",

530
composer.lock generated
View File

@@ -4,395 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "c3afb3b29d3f46223386d29810f6e8d5", "content-hash": "659b8e6e4e495bf62452eb85772ac9aa",
"packages": [ "packages": [
{
"name": "doctrine/cache",
"version": "1.13.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/cache.git",
"reference": "56cd022adb5514472cb144c087393c1821911d09"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/cache/zipball/56cd022adb5514472cb144c087393c1821911d09",
"reference": "56cd022adb5514472cb144c087393c1821911d09",
"shasum": ""
},
"require": {
"php": "~7.1 || ^8.0"
},
"conflict": {
"doctrine/common": ">2.2,<2.4"
},
"require-dev": {
"alcaeus/mongo-php-adapter": "^1.1",
"cache/integration-tests": "dev-master",
"doctrine/coding-standard": "^9",
"mongodb/mongodb": "^1.1",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
"predis/predis": "~1.0",
"psr/cache": "^1.0 || ^2.0 || ^3.0",
"symfony/cache": "^4.4 || ^5.4 || ^6",
"symfony/var-exporter": "^4.4 || ^5.4 || ^6"
},
"suggest": {
"alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver"
},
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.",
"homepage": "https://www.doctrine-project.org/projects/cache.html",
"keywords": [
"abstraction",
"apcu",
"cache",
"caching",
"couchdb",
"memcached",
"php",
"redis",
"xcache"
],
"support": {
"issues": "https://github.com/doctrine/cache/issues",
"source": "https://github.com/doctrine/cache/tree/1.13.0"
},
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache",
"type": "tidelift"
}
],
"abandoned": true,
"time": "2022-05-20T20:06:54+00:00"
},
{
"name": "doctrine/common",
"version": "3.5.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/common.git",
"reference": "d9ea4a54ca2586db781f0265d36bea731ac66ec5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/common/zipball/d9ea4a54ca2586db781f0265d36bea731ac66ec5",
"reference": "d9ea4a54ca2586db781f0265d36bea731ac66ec5",
"shasum": ""
},
"require": {
"doctrine/persistence": "^2.0 || ^3.0 || ^4.0",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"doctrine/coding-standard": "^9.0 || ^10.0",
"doctrine/collections": "^1",
"phpstan/phpstan": "^1.4.1",
"phpstan/phpstan-phpunit": "^1",
"phpunit/phpunit": "^7.5.20 || ^8.5 || ^9.0",
"squizlabs/php_codesniffer": "^3.0",
"symfony/phpunit-bridge": "^6.1",
"vimeo/psalm": "^4.4"
},
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\Common\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
},
{
"name": "Marco Pivetta",
"email": "ocramius@gmail.com"
}
],
"description": "PHP Doctrine Common project is a library that provides additional functionality that other Doctrine projects depend on such as better reflection support, proxies and much more.",
"homepage": "https://www.doctrine-project.org/projects/common.html",
"keywords": [
"common",
"doctrine",
"php"
],
"support": {
"issues": "https://github.com/doctrine/common/issues",
"source": "https://github.com/doctrine/common/tree/3.5.0"
},
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcommon",
"type": "tidelift"
}
],
"time": "2025-01-01T22:12:03+00:00"
},
{
"name": "doctrine/event-manager",
"version": "2.1.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/event-manager.git",
"reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/event-manager/zipball/dda33921b198841ca8dbad2eaa5d4d34769d18cf",
"reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"conflict": {
"doctrine/common": "<2.9"
},
"require-dev": {
"doctrine/coding-standard": "^14",
"phpdocumentor/guides-cli": "^1.4",
"phpstan/phpstan": "^2.1.32",
"phpunit/phpunit": "^10.5.58"
},
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\Common\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
},
{
"name": "Marco Pivetta",
"email": "ocramius@gmail.com"
}
],
"description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.",
"homepage": "https://www.doctrine-project.org/projects/event-manager.html",
"keywords": [
"event",
"event dispatcher",
"event manager",
"event system",
"events"
],
"support": {
"issues": "https://github.com/doctrine/event-manager/issues",
"source": "https://github.com/doctrine/event-manager/tree/2.1.1"
},
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager",
"type": "tidelift"
}
],
"time": "2026-01-29T07:11:08+00:00"
},
{
"name": "doctrine/persistence",
"version": "4.1.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/persistence.git",
"reference": "b9c49ad3558bb77ef973f4e173f2e9c2eca9be09"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/persistence/zipball/b9c49ad3558bb77ef973f4e173f2e9c2eca9be09",
"reference": "b9c49ad3558bb77ef973f4e173f2e9c2eca9be09",
"shasum": ""
},
"require": {
"doctrine/event-manager": "^1 || ^2",
"php": "^8.1",
"psr/cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"doctrine/coding-standard": "^14",
"phpstan/phpstan": "2.1.30",
"phpstan/phpstan-phpunit": "^2",
"phpstan/phpstan-strict-rules": "^2",
"phpunit/phpunit": "^10.5.58 || ^12",
"symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0",
"symfony/finder": "^4.4 || ^5.4 || ^6.0 || ^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\Persistence\\": "src/Persistence"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
},
{
"name": "Marco Pivetta",
"email": "ocramius@gmail.com"
}
],
"description": "The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.",
"homepage": "https://www.doctrine-project.org/projects/persistence.html",
"keywords": [
"mapper",
"object",
"odm",
"orm",
"persistence"
],
"support": {
"issues": "https://github.com/doctrine/persistence/issues",
"source": "https://github.com/doctrine/persistence/tree/4.1.1"
},
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fpersistence",
"type": "tidelift"
}
],
"time": "2025-10-16T20:13:18+00:00"
},
{ {
"name": "guzzlehttp/guzzle", "name": "guzzlehttp/guzzle",
"version": "7.10.0", "version": "7.10.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/guzzle/guzzle.git", "url": "https://github.com/guzzle/guzzle.git",
"reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" "reference": "aec528da477062d3af11f51e6b33402be233b21f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", "url": "https://api.github.com/repos/guzzle/guzzle/zipball/aec528da477062d3af11f51e6b33402be233b21f",
"reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", "reference": "aec528da477062d3af11f51e6b33402be233b21f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -410,8 +35,9 @@
"bamarni/composer-bin-plugin": "^1.8.2", "bamarni/composer-bin-plugin": "^1.8.2",
"ext-curl": "*", "ext-curl": "*",
"guzzle/client-integration-tests": "3.0.2", "guzzle/client-integration-tests": "3.0.2",
"guzzlehttp/test-server": "^0.3.2",
"php-http/message-factory": "^1.1", "php-http/message-factory": "^1.1",
"phpunit/phpunit": "^8.5.39 || ^9.6.20", "phpunit/phpunit": "^8.5.52 || ^9.6.34",
"psr/log": "^1.1 || ^2.0 || ^3.0" "psr/log": "^1.1 || ^2.0 || ^3.0"
}, },
"suggest": { "suggest": {
@@ -489,7 +115,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/guzzle/guzzle/issues", "issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/7.10.0" "source": "https://github.com/guzzle/guzzle/tree/7.10.4"
}, },
"funding": [ "funding": [
{ {
@@ -505,20 +131,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-08-23T22:36:01+00:00" "time": "2026-05-22T19:00:53+00:00"
}, },
{ {
"name": "guzzlehttp/promises", "name": "guzzlehttp/promises",
"version": "2.3.0", "version": "2.4.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/guzzle/promises.git", "url": "https://github.com/guzzle/promises.git",
"reference": "481557b130ef3790cf82b713667b43030dc9c957" "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", "url": "https://api.github.com/repos/guzzle/promises/zipball/09e8a212562fb1fb6a512c4156ed71525969d6c2",
"reference": "481557b130ef3790cf82b713667b43030dc9c957", "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -526,7 +152,7 @@
}, },
"require-dev": { "require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2", "bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.44 || ^9.6.25" "phpunit/phpunit": "^8.5.52 || ^9.6.34"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@@ -572,7 +198,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/guzzle/promises/issues", "issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/2.3.0" "source": "https://github.com/guzzle/promises/tree/2.4.1"
}, },
"funding": [ "funding": [
{ {
@@ -588,20 +214,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-08-22T14:34:08+00:00" "time": "2026-05-20T22:57:30+00:00"
}, },
{ {
"name": "guzzlehttp/psr7", "name": "guzzlehttp/psr7",
"version": "2.8.0", "version": "2.10.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/guzzle/psr7.git", "url": "https://github.com/guzzle/psr7.git",
"reference": "21dc724a0583619cd1652f673303492272778051" "reference": "a1bbdc172f32a25fe999965b65b6e71fd87da9ed"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", "url": "https://api.github.com/repos/guzzle/psr7/zipball/a1bbdc172f32a25fe999965b65b6e71fd87da9ed",
"reference": "21dc724a0583619cd1652f673303492272778051", "reference": "a1bbdc172f32a25fe999965b65b6e71fd87da9ed",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -616,8 +242,9 @@
}, },
"require-dev": { "require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2", "bamarni/composer-bin-plugin": "^1.8.2",
"http-interop/http-factory-tests": "0.9.0", "http-interop/http-factory-tests": "1.1.0",
"phpunit/phpunit": "^8.5.44 || ^9.6.25" "jshttp/mime-db": "1.54.0.1",
"phpunit/phpunit": "^8.5.52 || ^9.6.34"
}, },
"suggest": { "suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
@@ -688,7 +315,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/guzzle/psr7/issues", "issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.8.0" "source": "https://github.com/guzzle/psr7/tree/2.10.2"
}, },
"funding": [ "funding": [
{ {
@@ -704,7 +331,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-08-23T21:21:41+00:00" "time": "2026-05-25T22:58:15+00:00"
}, },
{ {
"name": "kevinrob/guzzle-cache-middleware", "name": "kevinrob/guzzle-cache-middleware",
@@ -1151,6 +778,57 @@
}, },
"time": "2024-09-11T13:17:53+00:00" "time": "2024-09-11T13:17:53+00:00"
}, },
{
"name": "psr/simple-cache",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/simple-cache.git",
"reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865",
"reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\SimpleCache\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interfaces for simple caching",
"keywords": [
"cache",
"caching",
"psr",
"psr-16",
"simple-cache"
],
"support": {
"source": "https://github.com/php-fig/simple-cache/tree/3.0.0"
},
"time": "2021-10-29T13:26:27+00:00"
},
{ {
"name": "ralouphie/getallheaders", "name": "ralouphie/getallheaders",
"version": "3.0.3", "version": "3.0.3",
@@ -1197,16 +875,16 @@
}, },
{ {
"name": "symfony/deprecation-contracts", "name": "symfony/deprecation-contracts",
"version": "v3.6.0", "version": "v3.7.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git", "url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b",
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -1219,7 +897,7 @@
"name": "symfony/contracts" "name": "symfony/contracts"
}, },
"branch-alias": { "branch-alias": {
"dev-main": "3.6-dev" "dev-main": "3.7-dev"
} }
}, },
"autoload": { "autoload": {
@@ -1244,7 +922,7 @@
"description": "A generic function and convention to trigger deprecation notices", "description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0"
}, },
"funding": [ "funding": [
{ {
@@ -1255,12 +933,16 @@
"url": "https://github.com/fabpot", "url": "https://github.com/fabpot",
"type": "github" "type": "github"
}, },
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-09-25T14:21:43+00:00" "time": "2026-04-13T15:52:40+00:00"
}, },
{ {
"name": "webmozart/assert", "name": "webmozart/assert",
@@ -2348,6 +2030,7 @@
"type": "github" "type": "github"
} }
], ],
"abandoned": true,
"time": "2023-02-03T06:58:43+00:00" "time": "2023-02-03T06:58:43+00:00"
}, },
{ {
@@ -2403,6 +2086,7 @@
"type": "github" "type": "github"
} }
], ],
"abandoned": true,
"time": "2023-02-03T06:59:15+00:00" "time": "2023-02-03T06:59:15+00:00"
}, },
{ {
@@ -3192,16 +2876,16 @@
}, },
{ {
"name": "symfony/http-foundation", "name": "symfony/http-foundation",
"version": "v7.4.5", "version": "v7.4.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/http-foundation.git", "url": "https://github.com/symfony/http-foundation.git",
"reference": "446d0db2b1f21575f1284b74533e425096abdfb6" "reference": "9381209597ec66c25be154cbf2289076e64d1eab"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/446d0db2b1f21575f1284b74533e425096abdfb6", "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9381209597ec66c25be154cbf2289076e64d1eab",
"reference": "446d0db2b1f21575f1284b74533e425096abdfb6", "reference": "9381209597ec66c25be154cbf2289076e64d1eab",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -3250,7 +2934,7 @@
"description": "Defines an object-oriented layer for the HTTP specification", "description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/http-foundation/tree/v7.4.5" "source": "https://github.com/symfony/http-foundation/tree/v7.4.8"
}, },
"funding": [ "funding": [
{ {
@@ -3270,20 +2954,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-01-27T16:16:02+00:00" "time": "2026-03-24T13:12:05+00:00"
}, },
{ {
"name": "symfony/polyfill-mbstring", "name": "symfony/polyfill-mbstring",
"version": "v1.33.0", "version": "v1.38.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git", "url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/14c5439eec4ccff081ac14eca2dc57feb2a66d92",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -3335,7 +3019,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.1"
}, },
"funding": [ "funding": [
{ {
@@ -3355,20 +3039,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-12-23T08:48:59+00:00" "time": "2026-05-26T12:51:13+00:00"
}, },
{ {
"name": "symfony/psr-http-message-bridge", "name": "symfony/psr-http-message-bridge",
"version": "v7.4.4", "version": "v7.4.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/psr-http-message-bridge.git", "url": "https://github.com/symfony/psr-http-message-bridge.git",
"reference": "929ffe10bbfbb92e711ac3818d416f9daffee067" "reference": "76f1a57719a4a04c0ea18678a6c9305b5dcb9da8"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/929ffe10bbfbb92e711ac3818d416f9daffee067", "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/76f1a57719a4a04c0ea18678a6c9305b5dcb9da8",
"reference": "929ffe10bbfbb92e711ac3818d416f9daffee067", "reference": "76f1a57719a4a04c0ea18678a6c9305b5dcb9da8",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -3423,7 +3107,7 @@
"psr-7" "psr-7"
], ],
"support": { "support": {
"source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.4.4" "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.4.8"
}, },
"funding": [ "funding": [
{ {
@@ -3443,7 +3127,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-01-03T23:30:35+00:00" "time": "2026-03-24T13:12:05+00:00"
}, },
{ {
"name": "theseer/tokenizer", "name": "theseer/tokenizer",

View File

@@ -10,121 +10,139 @@ use ML\IRI\IRI;
class COIDParserTest extends \PHPUnit\Framework\TestCase { class COIDParserTest extends \PHPUnit\Framework\TestCase {
public function testRootCOID() { public function testRootCOID() {
$coid = new IRI('coid://example.com'); $coid = new IRI('coid://example.com');
$this->assertEquals(COIDParser::COID_ROOT, COIDParser::getType($coid)); $this->assertEquals(COIDParser::COID_ROOT, COIDParser::getType($coid));
}
public function testInvalidRootCOID() { $coid = new IRI('coid://subdomain.example.com');
$coid = new IRI('coid://example'); $this->assertEquals(COIDParser::COID_ROOT, COIDParser::getType($coid));
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('coid://exämple.com');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('coid://ex&mple.com');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
}
public function testInvalidCOID() { $coid = new IRI('coid://anotherlevel.subdomain.example.com');
$coid = new IRI('http://example.com'); $this->assertEquals(COIDParser::COID_ROOT, COIDParser::getType($coid));
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid)); }
$coid = new IRI('example.com');
$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));
$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() { public function testInvalidRootCOID() {
$coid = new IRI('coid://example.com/Example'); $coid = new IRI('coid://example');
$this->assertEquals(COIDParser::COID_UNVERSIONED, COIDParser::getType($coid)); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
}
public function testInvalidUnversionedCOID() { $coid = new IRI('coid://exämple.com');
$coid = new IRI('coid://example.com/Exümple'); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('coid://example.com/Examp%e');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
}
public function testVersionedCOID() { $coid = new IRI('coid://ex&mple.com');
$coid = new IRI('coid://example.com/Example/1.0'); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$this->assertEquals(COIDParser::COID_VERSIONED, COIDParser::getType($coid)); }
$coid = new IRI('coid://example.com/Example/alpha');
$this->assertEquals(COIDParser::COID_VERSIONED, COIDParser::getType($coid));
}
public function testInvalidVersionedCOID() { public function testInvalidCOID() {
$coid = new IRI('coid://example.com/Example/1.$'); $coid = new IRI('http://example.com');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid)); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
}
public function testVersionWildcardCOID() { $coid = new IRI('example.com');
$coid = new IRI('coid://example.com/Example/^1.0'); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$this->assertEquals(COIDParser::COID_VERSION_WILDCARD, COIDParser::getType($coid));
$coid = new IRI('coid://example.com/Example/~1.0');
$this->assertEquals(COIDParser::COID_VERSION_WILDCARD, COIDParser::getType($coid));
$coid = new IRI('coid://example.com/Example/1.*');
$this->assertEquals(COIDParser::COID_VERSION_WILDCARD, COIDParser::getType($coid));
}
public function testInvalidVersionWildcardCOID() { $coid = new IRI('COID://example.com');
$coid = new IRI('coid://example.com/Example/^1.*'); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('coid://example.com/Example/1.a.*');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
}
public function testIRICaseSensitivity() { $coid = new IRI('Coid://example.com');
$coid1 = new IRI('coid://example.com/example/1.0'); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid2 = new IRI('coid://example.com/Example/1.0');
$this->assertFalse($coid1->equals($coid2));
}
public function testRootFromString() { $coid = new IRI('coid://EXAMPLE.COM');
$coid1 = new IRI('coid://example.com'); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid2 = COIDParser::fromString('coid://example.com');
$coid3 = COIDParser::fromString('example.com'); $coid = new IRI('coid://exAMPle.CoM');
$this->assertTrue($coid1->equals($coid2)); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$this->assertTrue($coid1->equals($coid3)); }
}
public function testUnversionedFromString() { public function testUnversionedCOID() {
$coid1 = new IRI('coid://example.com/Example'); $coid = new IRI('coid://subdomain.example.com/Example');
$coid2 = COIDParser::fromString('coid://example.com/Example'); $this->assertEquals(COIDParser::COID_UNVERSIONED, COIDParser::getType($coid));
$coid3 = COIDParser::fromString('example.com/Example'); }
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
}
public function testVersionedFromString() { public function testInvalidUnversionedCOID() {
$coid1 = new IRI('coid://example.com/Example/1.0'); $coid = new IRI('coid://example.com/Exümple');
$coid2 = COIDParser::fromString('coid://example.com/Example/1.0'); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid3 = COIDParser::fromString('example.com/Example/1.0');
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
}
public function testNormalizeRootFromString() { $coid = new IRI('coid://example.com/Examp%e');
$coid1 = new IRI('coid://example.com'); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid2 = COIDParser::fromString('COID://example.com'); }
$coid3 = COIDParser::fromString('ExAmple.COM');
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
}
public function testNormalizeNonRootFromString() { public function testVersionedCOID() {
$coid1 = new IRI('coid://example.com/Example'); $coid = new IRI('coid://anotherlevel.subdomain.example.com/Example/1.0');
$coid2 = COIDParser::fromString('COID://example.com/Example'); $this->assertEquals(COIDParser::COID_VERSIONED, COIDParser::getType($coid));
$coid3 = COIDParser::fromString('ExAmple.COM/Example');
$coid4 = COIDParser::fromString('ExAmple.COM/EXample'); $coid = new IRI('coid://subdomain.example.com/Example/alpha');
$this->assertTrue($coid1->equals($coid2)); $this->assertEquals(COIDParser::COID_VERSIONED, COIDParser::getType($coid));
$this->assertTrue($coid1->equals($coid3)); }
$this->assertFalse($coid1->equals($coid4));
} public function testInvalidVersionedCOID() {
$coid = new IRI('coid://example.com/Example/1.$');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
}
public function testVersionWildcardCOID() {
$coid = new IRI('coid://example.com/Example/^1.0');
$this->assertEquals(COIDParser::COID_VERSION_WILDCARD, COIDParser::getType($coid));
$coid = new IRI('coid://example.com/Example/~1.0');
$this->assertEquals(COIDParser::COID_VERSION_WILDCARD, COIDParser::getType($coid));
$coid = new IRI('coid://example.com/Example/1.*');
$this->assertEquals(COIDParser::COID_VERSION_WILDCARD, COIDParser::getType($coid));
}
public function testInvalidVersionWildcardCOID() {
$coid = new IRI('coid://example.com/Example/^1.*');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('coid://example.com/Example/1.a.*');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
}
public function testIRICaseSensitivity() {
$coid1 = new IRI('coid://example.com/example/1.0');
$coid2 = new IRI('coid://example.com/Example/1.0');
$this->assertFalse($coid1->equals($coid2));
}
public function testRootFromString() {
$coid1 = new IRI('coid://example.com');
$coid2 = COIDParser::fromString('coid://example.com');
$coid3 = COIDParser::fromString('example.com');
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
}
public function testUnversionedFromString() {
$coid1 = new IRI('coid://example.com/Example');
$coid2 = COIDParser::fromString('coid://example.com/Example');
$coid3 = COIDParser::fromString('example.com/Example');
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
}
public function testVersionedFromString() {
$coid1 = new IRI('coid://example.com/Example/1.0');
$coid2 = COIDParser::fromString('coid://example.com/Example/1.0');
$coid3 = COIDParser::fromString('example.com/Example/1.0');
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
}
public function testNormalizeRootFromString() {
$coid1 = new IRI('coid://example.com');
$coid2 = COIDParser::fromString('COID://example.com');
$coid3 = COIDParser::fromString('ExAmple.COM');
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
}
public function testNormalizeNonRootFromString() {
$coid1 = new IRI('coid://example.com/Example');
$coid2 = COIDParser::fromString('COID://example.com/Example');
$coid3 = COIDParser::fromString('ExAmple.COM/Example');
$coid4 = COIDParser::fromString('ExAmple.COM/EXample');
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
$this->assertFalse($coid1->equals($coid4));
}
} }

View File

@@ -0,0 +1,63 @@
<?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 ML\IRI\IRI;
use ReflectionProperty,
InvalidArgumentException;
class FunctionsTest extends \PHPUnit\Framework\TestCase {
protected function setUp(): void {
$prop = new ReflectionProperty(ObjectRetrieverFacade::class, 'instance');
$prop->setAccessible(true);
$prop->setValue(null, null);
}
private function makeMockRetrieverReturning(?CloudObject $object) : ObjectRetriever {
$mock = $this->createMock(ObjectRetriever::class);
$mock->method('getCloudObject')->willReturn($object);
return $mock;
}
public function testAcceptsIRI(): void {
$coid = new IRI('coid://cloudobjects.io');
$mockObject = $this->createMock(CloudObject::class);
$mockRetriever = $this->createMock(ObjectRetriever::class);
$mockRetriever->expects($this->once())
->method('getCloudObject')
->with($coid)
->willReturn($mockObject);
ObjectRetrieverFacade::setObjectRetriever($mockRetriever);
$this->assertSame($mockObject, cloudobjects_get_object($coid));
}
public function testConvertsStringToIRI(): void {
$mockObject = $this->createMock(CloudObject::class);
$mockRetriever = $this->createMock(ObjectRetriever::class);
$mockRetriever->expects($this->once())
->method('getCloudObject')
->with($this->callback(fn($arg) => $arg instanceof IRI && (string)$arg === 'coid://cloudobjects.io'))
->willReturn($mockObject);
ObjectRetrieverFacade::setObjectRetriever($mockRetriever);
$this->assertSame($mockObject, cloudobjects_get_object('coid://cloudobjects.io'));
}
public function testThrowsOnInvalidArgumentType(): void {
ObjectRetrieverFacade::setObjectRetriever($this->makeMockRetrieverReturning(null));
$this->expectException(InvalidArgumentException::class);
cloudobjects_get_object(42);
}
}

View File

@@ -126,6 +126,15 @@ class NodeReaderMockTest extends \PHPUnit\Framework\TestCase {
$this->assertEquals('coid://cloudobjects.io/Public', $object->getNode('co:isVisibleTo')->getId()); $this->assertEquals('coid://cloudobjects.io/Public', $object->getNode('co:isVisibleTo')->getId());
} }
public function testConstants() {
$coid = new IRI('coid://cloudobjects.io');
$this->useRootResourceMock();
$object = $this->retriever->getCloudObject($coid);
$this->assertEquals('6-fbea0c90b2c5e5300e4039ed99be9b2d', $object->getRevision());
$this->assertEquals('CloudObjects', $object->getLabel());
}
public function testGetAllValuesString1() { public function testGetAllValuesString1() {
$coid = new IRI('coid://cloudobjects.io'); $coid = new IRI('coid://cloudobjects.io');
$this->useRootResourceMock(); $this->useRootResourceMock();

View File

@@ -0,0 +1,76 @@
<?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 Exception;
use ML\IRI\IRI;
use ML\JsonLD\Node;
use ReflectionProperty;
class ObjectRetrieverFacadeTest extends \PHPUnit\Framework\TestCase {
protected function setUp(): void {
$prop = new ReflectionProperty(ObjectRetrieverFacade::class, 'instance');
$prop->setAccessible(true);
$prop->setValue(null, null);
}
public function testThrowsExceptionBeforeInitialization(): void {
$this->expectException(Exception::class);
ObjectRetrieverFacade::getObjectNode(new IRI('coid://cloudobjects.io'));
}
public function testForwardsGetObjectNodeToInstance(): void {
$coid = new IRI('coid://cloudobjects.io');
$mockNode = $this->createMock(Node::class);
$mockRetriever = $this->createMock(ObjectRetriever::class);
$mockRetriever->expects($this->once())
->method('getObjectNode')
->with($coid)
->willReturn($mockNode);
ObjectRetrieverFacade::setObjectRetriever($mockRetriever);
$this->assertSame($mockNode, ObjectRetrieverFacade::getObjectNode($coid));
}
public function testForwardsGetCloudObjectToInstance(): void {
$coid = new IRI('coid://cloudobjects.io');
$mockCloudObject = $this->createMock(CloudObject::class);
$mockRetriever = $this->createMock(ObjectRetriever::class);
$mockRetriever->expects($this->once())
->method('getCloudObject')
->with($coid)
->willReturn($mockCloudObject);
ObjectRetrieverFacade::setObjectRetriever($mockRetriever);
$this->assertSame($mockCloudObject, ObjectRetrieverFacade::getCloudObject($coid));
}
public function testReplacingRetrieverUsesNewInstance(): void {
$coid = new IRI('coid://cloudobjects.io');
$mockNode = $this->createMock(Node::class);
$firstRetriever = $this->createMock(ObjectRetriever::class);
$firstRetriever->expects($this->never())->method('getObjectNode');
$secondRetriever = $this->createMock(ObjectRetriever::class);
$secondRetriever->expects($this->once())
->method('getObjectNode')
->with($coid)
->willReturn($mockNode);
ObjectRetrieverFacade::setObjectRetriever($firstRetriever);
ObjectRetrieverFacade::setObjectRetriever($secondRetriever);
$this->assertSame($mockNode, ObjectRetrieverFacade::getObjectNode($coid));
}
}

View File

@@ -0,0 +1,47 @@
<?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 ML\IRI\IRI;
use Kevinrob\GuzzleCache\Storage\VolatileRuntimeStorage;
use CloudObjects\SDK\TestHelpers\InMemoryLogger;
class ObjectRetrieverCacheTest extends \PHPUnit\Framework\TestCase {
public function testCacheInRuntimeStorage() {
$cacheStorage = new VolatileRuntimeStorage;
$logger = new InMemoryLogger;
$coid = new IRI('coid://cloudobjects.io');
$retriever = new ObjectRetriever([
'cache_storage' => $cacheStorage,
'logger' => $logger
]);
$object1 = $retriever->getCloudObject($coid);
$this->assertNotNull($object1);
$this->assertNotNull($cacheStorage->fetch($retriever->getCacheKey((string)$coid)));
$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
]);
$object2 = $retriever->getCloudObject($coid);
$this->assertNotNull($object2);
$this->assertNotNull($cacheStorage->fetch($retriever->getCacheKey((string)$coid)));
$this->assertStringContainsString('from object cache', $logger->getLastLogMessage());
$this->assertEquals($object1->getRevision(), $object2->getRevision());
}
}