diff --git a/composer.json b/composer.json index f5daeaf..341fa30 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "doctrine/orm": "2.5.14", "friendica/json-ld": "^1.1", "guzzlehttp/guzzle": "^6.3", + "ml/json-ld": "1.1.0", "monolog/monolog": "^1.0", "phpseclib/phpseclib": "^2.0", "psr/http-message": "^1.0", @@ -42,9 +43,9 @@ "zendframework/zend-diactoros": "1.4.1" }, "require-dev": { + "ext-pdo": "*", "phpunit/dbunit": "^2.0", - "phpunit/phpunit": "^4.0", - "ext-pdo": "*" + "phpunit/phpunit": "^4.0" }, "autoload": { "psr-4": { diff --git a/src/JsonLd/Dereferencer/DereferencerInterface.php b/src/JsonLd/Dereferencer/DereferencerInterface.php new file mode 100644 index 0000000..9b465ad --- /dev/null +++ b/src/JsonLd/Dereferencer/DereferencerInterface.php @@ -0,0 +1,18 @@ +getGraph(); + $id = empty( $doc->getIri() ) ? '_:b0' : $doc->getIri(); + $this->node = $graph->getNode( $id ); + if ( is_null( $this->node ) ) { + $this->node = $graph->createNode(); + } + $this->context = $context; + } + + /** + * Cardinality-one get. Gets the single value for the property named $name. + * If there are multiple values defined for the property, only the first value is returned. + * @param string $name The property name to get. + * @return mixed A single property value. + * @throws PropertyNotDefinedException If no property named $name exists. + */ + public function get( $name ) + { + $property = $this->getNodeProperty( $name ); + if ( is_array( $property ) ) { + $property = $property[0]; + } + return $this->resolveProperty( $property ); + } + + /** + * Cardinality-many get. Gets all the values for the property named $name. + * If there is only one value defined for the property, it is returned as a length-1 array. + * @param string $name The property name to get. + * @return mixed A single property value. + * @throws PropertyNotDefinedException If no property named $name exists. + */ + public function getMany( $name ) + { + $property = $this->getNodeProperty( $name ); + if ( ! is_array( $property ) ) { + $property = array( $property ); + } + return $this->resolveProperty( $property ); + } + + /** + * A convenience wrapper around $this->get( $name ). Cardinality-one. + * @param string $name + * @return mixed + * @throws PropertyNotDefinedException + */ + public function __get( $name ) + { + return $this->get( $name ); + } + + /** + * Gets the value of the property named $name. + * @param string $name + * @return mixed + * @throws PropertyNotDefinedException if no property named $name exists. + */ + private function getNodeProperty( $name ) + { + $expandedName = $this->expand_name( $name ); + $property = $this->node->getProperty( $expandedName ); + if ( is_null( $property ) ) { + throw new PropertyNotDefinedException( $name ); + } + return $property; + } + + + /** + * Resolves the result of $this->node->getProperty() to something the application can use. + * @param mixed $property + * @return array|string + */ + private function resolveProperty( $property ) + { + if ( $property instanceof Value ) { + return $property->getValue(); + } else if ( is_array( $property ) ) { + return array_map( array( $this, 'resolveProperty' ), $property ); + } + // TODO handle lazy-loading linked nodes here + // also, figure out what to do about as:items -- the vocab says it should be a node but the JsonLD lib + // seems to resolve it to an array if it comes in as an array of string values... + } + + /** + * Sets the value for a new or existing property on the node. + * If the property already exists, the new value overwrites the old value(s). + * @param string $name + * @param string|\stdClass|array $value + */ + public function setProperty( $name, $value ) + { + $expandedName = $this->expand_name( $name ); + if ( $value instanceof \stdClass || is_array( $value ) ) { + // TODO handle adding a new linked node here + // should instantiate a new JsonLdNode and recursively call __set + } else if ( $value instanceof JsonLdNode ) { + // TODO handle adding a new linked node here + // by getting the \ML\JsonLD\Node instance from the $value and calling $this->node->addPropertyValue() + } else { + $this->node->setProperty( $expandedName, $value ); + } + } + + /** + * Convenience wrapper around $this->setProperty(). + * If the property already exists, the new value overwrites the old value(s). + * @param string $name + * @param string|\stdClass|array $value + */ + public function __set( $name, $value ) + { + return $this->setProperty( $name, $value ); + } + + /** + * Adds a new value to a new or existing property on the node. + * If the property already exists, the new value is added onto the existing values rather than + * overwriting them. + * @param string $name + * @param string|\stdClass|array $value + */ + public function addPropertyValue( $name, $value ) + { + $expandedName = $this->expand_name( $name ); + if ( $value instanceof \stdClass || is_array( $value ) ) { + // TODO handle adding a new linked node here + // should instantiate a new JsonLdNode and recursively call __set + } else if ( $value instanceof JsonLdNode ) { + // TODO handle adding a new linked node here + // by getting the \ML\JsonLD\Node instance from the $value and calling $this->node->addPropertyValue() + } else { + $this->node->addPropertyValue( $expandedName, $value ); + } + } + + /** + * Clears the property named $name, if it exists. + * @param string $name + */ + public function clearProperty( $name ) + { + return $this->setProperty( $name, null ); + } + + /** + * Resolves $name to a full IRI given the JSON-LD context of this node. + * @param string $name The name of the property to resolve. + * @return string The expanded name. + */ + private function expand_name( $name ) + { + $dummyObj = (object) array( + '@context' => $this->context, + $name => '_dummyValue', + ); + $expanded = (array) JsonLD::expand( $dummyObj )[0]; + return array_keys( $expanded )[0]; + } + + /** + * Whether a offset exists + * @link https://php.net/manual/en/arrayaccess.offsetexists.php + * @param mixed $offset
+ * An offset to check for. + *
+ * @return boolean true on success or false on failure. + * + *+ * The return value will be casted to boolean if non-boolean was returned. + * @since 5.0.0 + */ + public function offsetExists( $offset ) + { + $expandedName = $this->expand_name( (string) $offset ); + return !is_null( $this->node->getProperty( $expandedName ) ); + } + + /** + * Offset to retrieve + * @link https://php.net/manual/en/arrayaccess.offsetget.php + * @param mixed $offset
+ * The offset to retrieve. + *
+ * @return mixed Can return all value types. + * @since 5.0.0 + * @throws PropertyNotDefinedException + */ + public function offsetGet( $offset ) + { + return $this->get( (string) $offset ); + } + + /** + * Offset to set + * @link https://php.net/manual/en/arrayaccess.offsetset.php + * @param mixed $offset+ * The offset to assign the value to. + *
+ * @param mixed $value+ * The value to set. + *
+ * @return void + * @since 5.0.0 + */ + public function offsetSet( $offset, $value ) + { + return $this->setProperty( (string) $offset, $value ); + } + + /** + * Offset to unset + * @link https://php.net/manual/en/arrayaccess.offsetunset.php + * @param mixed $offset+ * The offset to unset. + *
+ * @return void + * @since 5.0.0 + */ + public function offsetUnset( $offset ) + { + return $this->clearProperty( (string) $offset ); + } +} \ No newline at end of file diff --git a/test/JsonLd/JsonLdNodeTest.php b/test/JsonLd/JsonLdNodeTest.php new file mode 100644 index 0000000..4667a2e --- /dev/null +++ b/test/JsonLd/JsonLdNodeTest.php @@ -0,0 +1,298 @@ + 'Object1', + ), + $this->asContext, + 'name', + 'Object1', + ), + array( + (object) array( + '@context' => array( + 'https://www.w3.org/ns/activitystreams', + ), + 'name' => 'Object2', + ), + $this->asContext, + 'name', + 'Object2', + ), + array( + (object) array( + 'https://www.w3.org/ns/activitystreams#name' => 'Object1', + ), + $this->asContext, + 'https://www.w3.org/ns/activitystreams#name', + 'Object1', + ), + array( + (object) array( + 'https://www.w3.org/ns/activitystreams#name' => 'Object1', + ), + $this->asContext, + 'foo', + null, + PropertyNotDefinedException::class, + ), + array( + (object) array( + 'https://www.w3.org/ns/activitystreams#name' => 'Object1', + ), + (object) array( + 'as' => 'https://www.w3.org/ns/activitystreams#' + ), + 'as:name', + 'Object1', + ), + array( + (object) array( + 'https://www.w3.org/ns/activitystreams#subject' => array( + 'https://example.org/item/1', + 'https://example.org/item/2', + ), + ), + $this->asContext, + 'subject', + 'https://example.org/item/1', + ), + ); + } + + /** + * @dataProvider provideForBasicGetProperty + */ + public function testBasicGetProperty( $inputObj, $context, $propertyName, $expectedValue, $expectedException = null ) + { + $node = $this->makeJsonLdNode( $inputObj, $context ); + if ( $expectedException ) { + $this->setExpectedException( $expectedException ); + } + $propertyValue = $node->get( $propertyName ); + $this->assertEquals( $expectedValue, $propertyValue ); + } + + /** + * @dataProvider provideForBasicGetProperty + */ + public function testBasicMagicGetProperty( $inputObj, $context, $propertyName, $expectedValue, $expectedException = null ) + { + $node = $this->makeJsonLdNode( $inputObj, $context ); + if ( $expectedException ) { + $this->setExpectedException( $expectedException ); + } + $propertyValue = $node->$propertyName; + $this->assertEquals( $expectedValue, $propertyValue ); + } + + /** + * @dataProvider provideForBasicGetProperty + */ + public function testBasicArrayAccessProperty( $inputObj, $context, $propertyName, $expectedValue, $expectedException = null ) + { + $node = $this->makeJsonLdNode( $inputObj, $context ); + if ( $expectedException ) { + $this->setExpectedException( $expectedException ); + } + $propertyValue = $node[$propertyName]; + $this->assertEquals( $expectedValue, $propertyValue ); + } + + public function provideForBasicGetMany() + { + return array( + array( + (object) array( + 'https://www.w3.org/ns/activitystreams#name' => 'Object1', + ), + $this->asContext, + 'name', + array( 'Object1' ), + ), + array( + (object) array( + '@context' => array( + 'https://www.w3.org/ns/activitystreams', + ), + 'name' => 'Object2', + ), + $this->asContext, + 'name', + array( 'Object2' ), + ), + array( + (object) array( + 'https://www.w3.org/ns/activitystreams#name' => 'Object1', + ), + $this->asContext, + 'https://www.w3.org/ns/activitystreams#name', + array( 'Object1' ), + ), + array( + (object) array( + 'https://www.w3.org/ns/activitystreams#name' => 'Object1', + ), + $this->asContext, + 'foo', + null, + PropertyNotDefinedException::class, + ), + array( + (object) array( + 'https://www.w3.org/ns/activitystreams#name' => 'Object1', + ), + (object) array( + 'as' => 'https://www.w3.org/ns/activitystreams#' + ), + 'as:name', + array( 'Object1' ), + ), + array( + (object) array( + 'https://www.w3.org/ns/activitystreams#subject' => array( + 'https://example.org/item/1', + 'https://example.org/item/2', + ), + ), + $this->asContext, + 'subject', + array( 'https://example.org/item/1', 'https://example.org/item/2' ), + ), + ); + } + + /** + * @dataProvider provideForBasicGetMany + */ + public function testBasicGetMany( $inputObj, $context, $propertyName, $expectedValue, $expectedException = null ) + { + $node = $this->makeJsonLdNode( $inputObj, $context ); + if ( $expectedException ) { + $this->setExpectedException( $expectedException ); + } + $propertyValue = $node->getMany( $propertyName ); + $this->assertEquals( $expectedValue, $propertyValue ); + } + + public function provideForBasicSetProperty() + { + return array( + array( + new stdClass(), + $this->asContext, + 'name', + 'NewName', + 'https://www.w3.org/ns/activitystreams#name', + 'NewName' + ), + array( + (object) array( + 'https://www.w3.org/ns/activitystreams#name' => 'OldName', + ), + $this->asContext, + 'name', + 'NewName', + 'https://www.w3.org/ns/activitystreams#name', + 'NewName' + ), + ); + } + + /** + * @dataProvider provideForBasicSetProperty + */ + public function testBasicSetProperty( $inputObj, $context, $propertyName, $newValue, $getPropertyName, $expectedValue, $expectedException = null ) + { + $node = $this->makeJsonLdNode( $inputObj, $context ); + if ( $expectedException ) { + $this->setExpectedException( $expectedException ); + } + $node->setProperty( $propertyName, $newValue ); + $this->assertEquals( $expectedValue, $node->$getPropertyName ); + } + + /** + * @dataProvider provideForBasicSetProperty + */ + public function testBasicMagicSetProperty( $inputObj, $context, $propertyName, $newValue, $getPropertyName, $expectedValue, $expectedException = null ) + { + $node = $this->makeJsonLdNode( $inputObj, $context ); + if ( $expectedException ) { + $this->setExpectedException( $expectedException ); + } + $node->$propertyName = $newValue; + $this->assertEquals( $expectedValue, $node->$getPropertyName ); + } + + /** + * @dataProvider provideForBasicSetProperty + */ + public function testBasicArraySetProperty( $inputObj, $context, $propertyName, $newValue, $getPropertyName, $expectedValue, $expectedException = null ) + { + $node = $this->makeJsonLdNode( $inputObj, $context ); + if ( $expectedException ) { + $this->setExpectedException( $expectedException ); + } + $node[$propertyName] = $newValue; + $this->assertEquals( $expectedValue, $node[$getPropertyName] ); + } + + public function provideForBasicAddPropertyValue() + { + return array( + array( + new stdClass(), + $this->asContext, + 'name', + 'NewName', + 'name', + array( 'NewName' ), + ), + array( + (object) array( + 'https://www.w3.org/ns/activitystreams#name' => 'OldName', + ), + $this->asContext, + 'name', + 'NewName', + 'name', + array( 'OldName', 'NewName' ), + ), + ); + } + + /** + * @dataProvider provideForBasicAddPropertyValue + */ + public function testBasicAddPropertyValue( $inputObj, $context, $propertyName, $newValue, $getPropertyName, $expectedValue, $expectedException = null ) + { + $node = $this->makeJsonLdNode( $inputObj, $context ); + if ( $expectedException ) { + $this->setExpectedException( $expectedException ); + } + $node->addPropertyValue( $propertyName, $newValue ); + $this->assertEquals( $expectedValue, $node->getMany( $getPropertyName ) ); + } + + private function makeJsonLdNode( $inputObj, $context ) + { + return new JsonLdNode( $inputObj, $context ); + } +} \ No newline at end of file