diff --git a/src/JsonLd/JsonLdNode.php b/src/JsonLd/JsonLdNode.php index 23929f3..f8054f9 100644 --- a/src/JsonLd/JsonLdNode.php +++ b/src/JsonLd/JsonLdNode.php @@ -3,6 +3,7 @@ namespace ActivityPub\JsonLd; use ActivityPub\JsonLd\Dereferencer\DereferencerInterface; +use ActivityPub\JsonLd\Exceptions\NodeNotFoundException; use ActivityPub\JsonLd\Exceptions\PropertyNotDefinedException; use ArrayAccess; use BadMethodCallException; @@ -47,7 +48,14 @@ class JsonLdNode implements ArrayAccess */ private $graph; - // TODO support backreferences + /** + * An array mapping property names to JsonLdNodes, where each entry + * represents a backreference from the node in the array to this node + * via named property (e.g. a backreference from "as:inReplyTo" => nodeA means + * that nodeA["as:inReplyTo"] == this node). + * @var array + */ + private $backreferences; /** * JsonLdNode constructor. @@ -57,7 +65,12 @@ class JsonLdNode implements ArrayAccess * @param DereferencerInterface $dereferencer * @param JsonLdGraph $graph The JSON-LD graph this node is a part of. */ - public function __construct( $jsonLd, $context, JsonLdNodeFactory $factory, DereferencerInterface $dereferencer, JsonLdGraph $graph ) + public function __construct( $jsonLd, + $context, + JsonLdNodeFactory $factory, + DereferencerInterface $dereferencer, + JsonLdGraph $graph, + $backreferences = array() ) { $this->factory = $factory; $this->dereferencer = $dereferencer; @@ -73,6 +86,7 @@ class JsonLdNode implements ArrayAccess $this->context = $context; $this->graph = $graph; $this->graph->addNode( $this ); + $this->backreferences = $backreferences; } /** @@ -103,12 +117,13 @@ class JsonLdNode implements ArrayAccess * @param string $name The property name to get. * @return mixed A single property value. * @throws PropertyNotDefinedException If no property named $name exists. + * @throws NodeNotFoundException If lazy-loading a linked node fails. */ public function get( $name ) { $expandedName = $this->expandName( $name ); if ( property_exists( $this->expanded, $expandedName ) ) { - return $this->resolveProperty( $this->expanded->$expandedName[0] ); + return $this->resolveProperty( $expandedName, $this->expanded->$expandedName[0] ); } throw new PropertyNotDefinedException( $name ); } @@ -119,12 +134,13 @@ class JsonLdNode implements ArrayAccess * @param string $name The property name to get. * @return mixed A single property value. * @throws PropertyNotDefinedException If no property named $name exists. + * @throws NodeNotFoundException If lazy-loading a linked node fails. */ public function getMany( $name ) { $expandedName = $this->expandName( $name ); if ( property_exists( $this->expanded, $expandedName ) ) { - return $this->resolveProperty( $this->expanded->$expandedName ); + return $this->resolveProperty( $expandedName, $this->expanded->$expandedName ); } throw new PropertyNotDefinedException( $name ); } @@ -140,15 +156,28 @@ class JsonLdNode implements ArrayAccess return $this->get( $name ); } - private function resolveProperty( &$property ) + /** + * Takes a raw value from $this->expanded and turns it into something useful. + * If its a node, lazy-load the node and wrap it in a JsonLdNode. If it's a value object, + * return the value. If it's an array, recursively resolve the values in the array. + * @param string $expandedName The name of the property we are resolving. + * @param mixed $property The property value. + * @return JsonLdNode|array|mixed + * @throws NodeNotFoundException If lazy-loading the node fails because the node does not exist. + */ + private function resolveProperty( $expandedName, &$property ) { if ( is_array( $property ) ) { - return array_map( array( $this, 'resolveProperty'), $property ); + for ( $i = 0; $i < count( $property); $i += 1) { + $names[] = $expandedName; + } + return array_map( array( $this, 'resolveProperty'), $names, $property ); } else if ( $property instanceof stdClass && property_exists( $property, '@id') ) { // Only dereference if @id is the only property present if ( count( get_object_vars( $property ) ) > 1 ) { return $property; } + // Otherwise lazy-load the referenced node $idProp = '@id'; $iri = $property->$idProp; $dereferenced = $this->dereferencer->dereference( $iri ); @@ -156,7 +185,8 @@ class JsonLdNode implements ArrayAccess $property = $expanded; $referencedNode = $this->graph->getNode( $property->$idProp ); if ( is_null( $referencedNode) ) { - $referencedNode = $this->factory->newNode( $property, $this->graph ); + $backrefs = array( $expandedName => array( $this ) ); + $referencedNode = $this->factory->newNode( $property, $this->graph, $backrefs ); } return $referencedNode; } else if ( $property instanceof stdClass && property_exists( $property, '@value' ) ) { @@ -183,6 +213,7 @@ class JsonLdNode implements ArrayAccess throw new InvalidArgumentException( 'This node already has an id.' ); } $expandedValue = $this->expandValue( $expandedName, $value ); + $this->addNewValueToGraph( $expandedName, $expandedValue ); $this->expanded->$expandedName = $expandedValue; if ( $expandedName === '@id' ) { $this->graph->nameBlankNode( $this->getId(), $expandedValue ); @@ -197,6 +228,7 @@ class JsonLdNode implements ArrayAccess throw new InvalidArgumentException( 'Cannot add to the @id property.' ); } $expandedValue = $this->expandValue( $expandedName, $value ); + $this->addNewValueToGraph( $expandedName, $expandedValue ); if ( property_exists( $this->expanded, $expandedName ) ) { $this->expanded->$expandedName = array_merge( $this->expanded->$expandedName, $expandedValue ); } else { @@ -204,6 +236,26 @@ class JsonLdNode implements ArrayAccess } } + private function addNewValueToGraph( $expandedName, $expandedValue ) + { + if ( is_array( $expandedValue ) ) { + for ( $i = 0; $i < count( $expandedValue ); $i += 1 ) { + $names[] = $expandedName; + } + array_map( array( $this, 'addNewValueToGraph' ), $names, $expandedValue ); + } else if ( $expandedValue instanceof stdClass && property_exists( $expandedValue, '@id' ) ) { + $idProp = '@id'; + $id = $expandedValue->$idProp; + $referencedNode = $this->graph->getNode( $id ); + if ( is_null( $referencedNode ) ) { + $backrefs = array( $expandedName => array( $this ) ); + $referencedNode = $this->factory->newNode( $expandedValue, $this->graph, $backrefs ); + } else { + $referencedNode->addBackReference( $expandedName, $this ); + } + } + } + /** * Convenience wrapper around $this->set(). * If the property already exists, the new value overwrites the old value(s). @@ -263,6 +315,11 @@ class JsonLdNode implements ArrayAccess return property_exists( $this->expanded, '@id' ); } + public function addBackReference( $expandedName, JsonLdNode $referencingNode ) + { + $this->backreferences[$expandedName][] = $referencingNode; + } + /** * Whether a offset exists * @link https://php.net/manual/en/arrayaccess.offsetexists.php diff --git a/src/JsonLd/JsonLdNodeFactory.php b/src/JsonLd/JsonLdNodeFactory.php index 3bd835e..c47e3b8 100644 --- a/src/JsonLd/JsonLdNodeFactory.php +++ b/src/JsonLd/JsonLdNodeFactory.php @@ -33,13 +33,14 @@ class JsonLdNodeFactory * Construct and return a new JsonLdNode. * @param Node|\stdClass $jsonLd The JSON-LD object input. * @param JsonLdGraph|null $graph The JSON-LD graph. + * @param array $backreferences Backreferences to instantiate the new node with. * @return JsonLdNode */ - public function newNode( $jsonLd, $graph = null ) + public function newNode( $jsonLd, $graph = null, $backreferences = array() ) { if ( is_null( $graph ) ) { $graph = new JsonLdGraph(); } - return new JsonLdNode( $jsonLd, $this->context, $this, $this->dereferencer, $graph ); + return new JsonLdNode( $jsonLd, $this->context, $this, $this->dereferencer, $graph, $backreferences ); } } \ No newline at end of file diff --git a/test/JsonLd/JsonLdNodeTest.php b/test/JsonLd/JsonLdNodeTest.php index 286d983..f2ed4e8 100644 --- a/test/JsonLd/JsonLdNodeTest.php +++ b/test/JsonLd/JsonLdNodeTest.php @@ -2,6 +2,7 @@ namespace ActivityPub\Test\JsonLd; +use ActivityPub\JsonLd\Exceptions\NodeNotFoundException; use ActivityPub\JsonLd\Exceptions\PropertyNotDefinedException; use ActivityPub\JsonLd\JsonLdNode; use ActivityPub\JsonLd\JsonLdNodeFactory; @@ -316,6 +317,70 @@ class JsonLdNodeTest extends APTestCase 'type' => 'Note', ), ), + array( + (object) array( + '@context' => array( 'https://www.w3.org/ns/activitystreams' ), + 'type' => 'Announce', + 'object' => 'https://example.org/objects/1', + ), + $this->asContext, + array(), + 'object', + null, + NodeNotFoundException::class, + ), + array( + (object) array( + '@context' => array( 'https://www.w3.org/ns/activitystreams' ), + 'type' => 'Announce', + 'object' => 'https://example.org/objects/1', + ), + $this->asContext, + array( + 'https://example.org/objects/1' => (object) array( + '@context' => array( 'https://www.w3.org/ns/activitystreams' ), + 'id' => 'https://example.org/objects/1', + 'type' => 'Note', + 'inReplyTo' => (object) array( + 'id' => 'https://example.org/articles/1', + 'type' => 'Article', + ), + ), + ), + 'object', + (object) array( + '@context' => array( 'https://www.w3.org/ns/activitystreams' ), + 'id' => 'https://example.org/objects/1', + 'type' => 'Note', + 'inReplyTo' => (object) array( + 'id' => 'https://example.org/articles/1', + 'type' => 'Article', + ), + ), + ), + array( + (object) array( + '@context' => array( 'https://www.w3.org/ns/activitystreams' ), + 'type' => 'Announce', + 'object' => 'https://example.org/objects/1', + ), + $this->asContext, + array( + 'https://example.org/objects/1' => (object) array( + '@context' => array( 'https://www.w3.org/ns/activitystreams' ), + 'id' => 'https://example.org/objects/1', + 'type' => 'Note', + 'inReplyTo' => 'https://example.org/articles/1', + ), + ), + 'object', + (object) array( + '@context' => array( 'https://www.w3.org/ns/activitystreams' ), + 'id' => 'https://example.org/objects/1', + 'type' => 'Note', + 'inReplyTo' => 'https://example.org/articles/1', + ), + ), ); } @@ -335,6 +400,12 @@ class JsonLdNodeTest extends APTestCase $this->assertEquals( $expectedValue, $actualValue ); } + public function testBackreferences() + { + // TODO implement me + $this->assertTrue( false ); + } + private function makeJsonLdNode( $inputObj, $context, $nodeGraph = array() ) { $factory = new JsonLdNodeFactory( $context, new TestDereferencer( $nodeGraph ) );