diff --git a/lib/RdfNamespace.php b/lib/RdfNamespace.php index 7fdd7c4c..0c4da0bf 100644 --- a/lib/RdfNamespace.php +++ b/lib/RdfNamespace.php @@ -108,7 +108,10 @@ class RdfNamespace 'xsd' => 'http://www.w3.org/2001/XMLSchema#', ]; - private static $namespaces; + /** + * @var array|null array with prefixes as key and related IRI as value + */ + private static array|null $namespaces = null; private static $default; @@ -118,7 +121,7 @@ class RdfNamespace /** * Return all the namespaces registered * - * @return array associative array of all the namespaces + * @return array Associative array of all the namespaces. Key is prefix, value is long URI. */ public static function namespaces() { @@ -138,6 +141,50 @@ public static function resetNamespaces() self::$namespaces = self::$initial_namespaces; } + /** + * @param string $prefix + * + * @throws \InvalidArgumentException if prefix is not a string + * @throws \InvalidArgumentException if prefix does not match RDFXML-QName specification + * @throws \LogicException if preg_match returned false when checking the given prefix + */ + private static function verifyPrefix($prefix): void + { + if (\is_string($prefix) && '' !== $prefix) { + // prefix ::= Name minus ":" // see: http://www.w3.org/TR/REC-xml-names/#NT-NCName + // Name ::= NameStartChar (NameChar)* // see: http://www.w3.org/TR/REC-xml/#NT-Name + // NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | + // [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | + // [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF] + // NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040] + + $_name_start_char = + 'A-Z_a-z\xc0-\xD6\xd8-\xf6\xf8-\xff\x{0100}-\x{02ff}\x{0370}-\x{037d}'. + '\x{037F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}'. + '\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}'; + + $_name_char = + $_name_start_char. + '\-.0-9\xb7\x{0300}-\x{036f}\x{203f}-\x{2040}'; + + $regex = "#^[{$_name_start_char}]{1}[{$_name_char}]{0,}$#u"; + + $match_result = preg_match($regex, $prefix); + + if (false === $match_result) { + throw new \LogicException('regexp error'); + } + + if (0 === $match_result) { + throw new \InvalidArgumentException("\$prefix should match RDFXML-QName specification. got: {$prefix}"); + } + } elseif ('' === $prefix) { + // empty prefix + } else { + throw new \InvalidArgumentException('$prefix should be a string and cannot be null or empty'); + } + } + /** * Return a namespace given its prefix. * @@ -149,15 +196,7 @@ public static function resetNamespaces() */ public static function get($prefix) { - // TODO fix PHPStan error by rethinking datatype of parameter(s) - // @phpstan-ignore-next-line - if (!\is_string($prefix) || null === $prefix) { - throw new \InvalidArgumentException('$prefix should be a string and cannot be null or empty'); - } - - if (preg_match('/\W/', $prefix)) { - throw new \InvalidArgumentException('$prefix should only contain alpha-numeric characters'); - } + self::verifyPrefix($prefix); $prefix = strtolower($prefix); $namespaces = self::namespaces(); @@ -180,54 +219,18 @@ public static function get($prefix) */ public static function set($prefix, $long) { - // TODO fix PHPStan error by rethinking datatype of parameter(s) - // @phpstan-ignore-next-line - if (!\is_string($prefix) || null === $prefix) { - throw new \InvalidArgumentException('$prefix should be a string and cannot be null or empty'); - } - - if ('' !== $prefix) { - // prefix ::= Name minus ":" // see: http://www.w3.org/TR/REC-xml-names/#NT-NCName - // Name ::= NameStartChar (NameChar)* // see: http://www.w3.org/TR/REC-xml/#NT-Name - // NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | - // [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | - // [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF] - // NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040] + self::verifyPrefix($prefix); - $_name_start_char = - 'A-Z_a-z\xc0-\xD6\xd8-\xf6\xf8-\xff\x{0100}-\x{02ff}\x{0370}-\x{037d}'. - '\x{037F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}'. - '\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}'; + if (\is_string($long) && '' !== $long) { + $prefix = strtolower($prefix); - $_name_char = - $_name_start_char. - '\-.0-9\xb7\x{0300}-\x{036f}\x{203f}-\x{2040}'; - - $regex = "#^[{$_name_start_char}]{1}[{$_name_char}]{0,}$#u"; - - $match_result = preg_match($regex, $prefix); - - if (false === $match_result) { - throw new \LogicException('regexp error'); - } - - if (0 === $match_result) { - throw new \InvalidArgumentException("\$prefix should match RDFXML-QName specification. got: {$prefix}"); - } - } + $namespaces = self::namespaces(); + $namespaces[$prefix] = $long; - // TODO fix PHPStan error by rethinking datatype of parameter(s) - // @phpstan-ignore-next-line - if (!\is_string($long) || null === $long || '' === $long) { + self::$namespaces = $namespaces; + } else { throw new \InvalidArgumentException('$long should be a string and cannot be null or empty'); } - - $prefix = strtolower($prefix); - - $namespaces = self::namespaces(); - $namespaces[$prefix] = $long; - - self::$namespaces = $namespaces; } /** @@ -284,11 +287,7 @@ public static function setDefault($namespace) */ public static function delete($prefix) { - // TODO fix PHPStan error by rethinking datatype of parameter(s) - // @phpstan-ignore-next-line - if (!\is_string($prefix) || null === $prefix || '' === $prefix) { - throw new \InvalidArgumentException('$prefix should be a string and cannot be null or empty'); - } + self::verifyPrefix($prefix); $prefix = strtolower($prefix); self::namespaces(); // make sure, that self::$namespaces is initialized @@ -320,7 +319,7 @@ public static function reset() * @param string|\EasyRdf\Resource $uri The full URI (eg 'http://xmlns.com/foaf/0.1/name') * @param bool $createNamespace If true, a new namespace will be created * - * @return array|null The split URI (eg 'foaf', 'name') or null + * @return array{string,string}|null The split URI (eg 'foaf', 'name') or null * * @throws \InvalidArgumentException */ @@ -335,7 +334,8 @@ public static function splitUri($uri, $createNamespace = false) if (\is_object($uri) && ($uri instanceof Resource)) { $uri = $uri->getUri(); - } elseif (!\is_string($uri)) { + // @phpstan-ignore-next-line + } elseif (false === \is_string($uri)) { throw new \InvalidArgumentException('$uri should be a string or EasyRdf\Resource'); } @@ -413,27 +413,25 @@ public static function shorten($uri, $createNamespace = false) * * @return string The full URI (eg 'http://xmlns.com/foaf/0.1/name') * - * @throws \InvalidArgumentException + * @throws \InvalidArgumentException if $shortUri is not a string or null or empty string */ public static function expand($shortUri) { - if (!\is_string($shortUri) || '' === $shortUri) { - throw new \InvalidArgumentException('$shortUri should be a string and cannot be null or empty'); - } - - if ('a' === $shortUri) { - $namespaces = self::namespaces(); - - return $namespaces['rdf'].'type'; - } elseif (preg_match('/^(\w+?):([\w\-]+)$/', $shortUri, $matches)) { - $long = self::get($matches[1]); - if ($long) { - return $long.$matches[2]; + if (\is_string($shortUri) && '' !== $shortUri) { + if ('a' === $shortUri) { + return self::namespaces()['rdf'].'type'; + } elseif (preg_match('/^([\w\-]+?):([\w\-]+)$/', $shortUri, $matches)) { + $long = self::get($matches[1]); + if ($long) { + return $long.$matches[2]; + } + } elseif (1 === preg_match('/^([\w\-]+)$/', $shortUri) && isset(self::$default)) { + return self::$default.$shortUri; } - } elseif (preg_match('/^(\w+)$/', $shortUri) && isset(self::$default)) { - return self::$default.$shortUri; - } - return $shortUri; + return $shortUri; + } else { + throw new \InvalidArgumentException('$shortUri should be a string and cannot be null or empty'); + } } } diff --git a/tests/EasyRdf/RdfNamespaceTest.php b/tests/EasyRdf/RdfNamespaceTest.php index ffd8c0ae..24ca6dc5 100644 --- a/tests/EasyRdf/RdfNamespaceTest.php +++ b/tests/EasyRdf/RdfNamespaceTest.php @@ -138,9 +138,7 @@ public function testGetNonStringNamespace() public function testGetNonAlphanumeric() { $this->expectException('InvalidArgumentException'); - $this->expectExceptionMessage( - '$prefix should only contain alpha-numeric characters' - ); + $this->expectExceptionMessage('$prefix should match RDFXML-QName specification. got: /K.O/'); RdfNamespace::get('/K.O/'); } @@ -236,11 +234,14 @@ public function testDeleteNamespace() public function testDeleteEmptyNamespace() { - $this->expectException('InvalidArgumentException'); - $this->expectExceptionMessage( - '$prefix should be a string and cannot be null or empty' - ); + $ns = 'http://empty/namespace'; + RdfNamespace::set('', $ns); + + $this->assertEquals($ns, RdfNamespace::get('')); + RdfNamespace::delete(''); + + $this->assertNull(RdfNamespace::get('')); } public function testDeleteNullNamespace() @@ -661,6 +662,16 @@ public function testExpandNonString() RdfNamespace::expand($this); } + /** + * @see https://github.com/sweetrdf/easyrdf/issues/32#issuecomment-1678073874 + */ + public function testExpandStringContainsHyphen() + { + RdfNamespace::set('foo-bar', 'http://long/uri/'); + + $this->assertEquals('http://long/uri/12', RdfNamespace::expand('foo-bar:12')); + } + /** * @see https://github.com/easyrdf/easyrdf/issues/185 */ @@ -684,6 +695,17 @@ public function testShortNamespace() ); } + /** + * @see https://github.com/sweetrdf/easyrdf/issues/32 + */ + public function testIssue32HyphenInName() + { + $url = 'http://example.org/dash#'; + + RdfNamespace::set('foo-bar', $url); + $this->assertSame($url, RdfNamespace::get('foo-bar')); + } + /** * URIs with fragments can only be shortened if the '#' character * is part of the prefix. `prefix:[...]#fragment` is not a valid result diff --git a/tests/EasyRdf/ResourceTest.php b/tests/EasyRdf/ResourceTest.php index bce78d7e..35a4f3c8 100644 --- a/tests/EasyRdf/ResourceTest.php +++ b/tests/EasyRdf/ResourceTest.php @@ -1081,6 +1081,24 @@ public function testTypeAsResource() ); } + /** + * Test type() together with a prefix that contains a hyphen. + * + * @see https://github.com/sweetrdf/easyrdf/issues/32#issuecomment-1678073874 + */ + public function testTypeWithHyphen() + { + RdfNamespace::set('foo-bar', 'http://foo/bar#'); + + $this->graph = new Graph(); + $this->type = $this->graph->resource('foo-bar:Person'); + $this->resource = $this->graph->resource('http://example.com/#me'); + + $this->graph->set($this->resource, 'rdf:type', $this->type); + + $this->assertEquals('foo-bar:Person', $this->resource->type()); + } + public function testIsA() { $this->setupTestGraph();