[WIP] Redo JsonLdNode implementation for better control

This commit is contained in:
Jeremy Dormitzer 2019-04-27 23:37:19 -04:00
parent 4913dfdf43
commit 3f0892a4a8
5 changed files with 319 additions and 147 deletions

View File

@ -0,0 +1,64 @@
<?php
namespace ActivityPub\JsonLd;
use InvalidArgumentException;
/**
* A view of a JSON-LD graph. Maps ids to JsonLdNode instances.
* Class JsonLdGraph
* @package ActivityPub\JsonLd
*/
class JsonLdGraph
{
/**
* @var int
*/
private $nextBlankId;
/**
* @var array
*/
private $graph;
public function __construct()
{
$this->nextBlankId = 0;
$this->graph = array();
}
public function addNode( JsonLdNode $node )
{
$id = $node->getId();
if ( is_null( $id ) ) {
$id = $this->getNextBlankId();
$node->setId( $id );
}
$this->graph[$id] = $node;
}
public function getNode( $id )
{
if ( array_key_exists( $id, $this->graph ) ) {
return $this->graph[$id];
}
}
public function nameBlankNode( $blankNodeName, $newNodeName ) {
if ( array_key_exists( $newNodeName, $this->graph ) ) {
throw new InvalidArgumentException( "$newNodeName is already defined." );
}
if ( ! array_key_exists( $blankNodeName, $this->graph ) ) {
throw new InvalidArgumentException( "$blankNodeName is not in the graph." );
}
$this->graph[$newNodeName] = $this->graph[$blankNodeName];
unset( $this->graph[$blankNodeName] );
}
private function getNextBlankId()
{
$nextId = $this->nextBlankId;
$this->nextBlankId += 1;
return "_:b$nextId";
}
}

View File

@ -5,32 +5,30 @@ namespace ActivityPub\JsonLd;
use ActivityPub\JsonLd\Dereferencer\DereferencerInterface;
use ActivityPub\JsonLd\Exceptions\PropertyNotDefinedException;
use ArrayAccess;
use BadMethodCallException;
use InvalidArgumentException;
use ML\JsonLD\Graph;
use ML\JsonLD\JsonLD;
use ML\JsonLD\Node;
use ML\JsonLD\Value;
use stdClass;
/**
* Class JsonLdNode
* @package ActivityPub\JsonLd
*
* A representation of a node in a JSON-LD graph. Supports lazy-loading linked nodes.
*/
class JsonLdNode implements ArrayAccess
{
/**
* The Node within $this->graph that represents this JsonLdNode.
* @var Node
* This node's id. May be null or a temporary id if this is a blank node.
* @var string|null
*/
private $node;
private $id;
/**
* The portion of the JSON-LD graph that this node knows about.
* @var Graph
* The JSON-LD expanded representation of the node.
* @var stdClass
*/
private $graph;
private $expanded;
/**
* The JSON-LD context.
* @var array|stdClass|string
*/
private $context;
/**
* The factory used to construct this node.
@ -39,37 +37,64 @@ class JsonLdNode implements ArrayAccess
private $factory;
/**
* The JSON-LD context that should be used when getting/setting properties on this node.
* @var array|\stdClass|string
*/
private $context;
/**
* The dereferencer, used to dereference foreign nodes based on their IRIs.
* @var DereferencerInterface
*/
private $dereferencer;
/**
* JsonLdNode constructor.
* @param Node|\stdClass $jsonLd The JSON-LD input as a stdClass or an existing \ML\JsonLD\Node instance.
* @param string $context This node's JSON-LD context.
* @param DereferencerInterface $dereferencer
* This node's view of the JSON-LD graph.
* @var JsonLdGraph
*/
public function __construct( $jsonLd, $context, JsonLdNodeFactory $factory, DereferencerInterface $dereferencer )
private $graph;
// TODO support backreferences
/**
* JsonLdNode constructor.
* @param stdClass $jsonLd The JSON-LD input.
* @param string|array|stdClass $context The JSON-LD context.
* @param JsonLdNodeFactory $factory The factory used to construct this instance.
* @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 )
{
$this->factory = $factory;
$this->dereferencer = $dereferencer;
$this->context = $context;
if ( $jsonLd instanceof Node ) {
$this->node = $jsonLd;
$this->graph = $jsonLd->getGraph();
if ( $jsonLd == new stdClass() ) {
$this->expanded = new stdClass();
} else {
$doc = JsonLD::getDocument( $jsonLd );
$this->graph = $doc->getGraph();
$nodes = $this->graph->getNodes();
$this->node = count( $nodes ) > 0 ? $nodes[0] : $this->graph->createNode();
$this->expanded = JsonLD::expand( $jsonLd )[0];
}
if ( property_exists( $this->expanded, '@id' ) ) {
$idProp = '@id';
$this->id = $this->expanded->$idProp;
}
$this->context = $context;
$this->graph = $graph;
$this->graph->addNode( $this );
}
/**
* Gets this node's id, if it has one. Could be null or a temporary id if this is a blank node.
* @return string|null
*/
public function getId()
{
return $this->id;
}
/**
* Sets this node's ID to $id.
* @param string $id
* @throws BadMethodCallException If this node already has an ID set.
*/
public function setId( $id )
{
if ( ! is_null( $this->getId() ) ) {
throw new BadMethodCallException( 'Node already has an ID' );
}
$this->id = $id;
}
/**
@ -81,11 +106,11 @@ class JsonLdNode implements ArrayAccess
*/
public function get( $name )
{
$property = $this->getNodeProperty( $name );
if ( is_array( $property ) ) {
$property = $property[0];
$expandedName = $this->expandName( $name );
if ( property_exists( $this->expanded, $expandedName ) ) {
return $this->resolveProperty( $this->expanded->$expandedName[0] );
}
return $this->resolveProperty( $property );
throw new PropertyNotDefinedException( $name );
}
/**
@ -97,11 +122,11 @@ class JsonLdNode implements ArrayAccess
*/
public function getMany( $name )
{
$property = $this->getNodeProperty( $name );
if ( ! is_array( $property ) ) {
$property = array( $property );
$expandedName = $this->expandName( $name );
if ( property_exists( $this->expanded, $expandedName ) ) {
return $this->resolveProperty( $this->expanded->$expandedName );
}
return $this->resolveProperty( $property );
throw new PropertyNotDefinedException( $name );
}
/**
@ -115,140 +140,127 @@ class JsonLdNode implements ArrayAccess
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 )
private function resolveProperty( &$property )
{
$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 );
} else if ( $property instanceof Node ) {
if ( count( $property->getProperties() ) > 0 ) {
return $this->factory->newNode( $property );
} else {
// dereference the node to get its properties, then update $property's props with the retrieved values
$dereferenced = $this->dereferencer->dereference( $property->getId() );
$newNode = JsonLD::getDocument( $dereferenced )->getGraph()->getNode( $property->getId() );
foreach ( $newNode->getProperties() as $name => $value ) {
$property->setProperty( $name, $value );
}
return $this->factory->newNode( $property );
if ( is_array( $property ) ) {
return array_map( array( $this, 'resolveProperty'), $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;
}
$idProp = '@id';
$iri = $property->$idProp;
$dereferenced = $this->dereferencer->dereference( $iri );
$expanded = JsonLD::expand( $dereferenced )[0];
$property = $expanded;
$referencedNode = $this->graph->getNode( $property->$idProp );
if ( is_null( $referencedNode) ) {
$referencedNode = $this->factory->newNode( $property, $this->graph );
}
return $referencedNode;
} else if ( $property instanceof stdClass && property_exists( $property, '@value' ) ) {
$value = '@value';
return $property->$value;
} else if ( $property instanceof stdClass ) {
$referencedNode = $this->factory->newNode( $property, $this->graph );
return $referencedNode;
} else {
return $property;
}
// TODO 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
* @param string|stdClass|array $value
*/
public function setProperty( $name, $value )
public function set( $name, $value )
{
$expandedName = $this->expand_name( $name );
if ( is_array( $value ) ) {
$this->clearProperty( $expandedName );
foreach ( $value as $v ) {
$this->addPropertyValue( $expandedName, $v );
}
} else if ( $value instanceof stdClass ) {
$newDoc = JsonLD::getDocument( $value );
$newNodes = $newDoc->getGraph()->getNodes();
$newNode = count( $newNodes ) > 0 ? $newNodes[0] : $this->graph->createNode();
$this->node->setProperty( $expandedName, $newNode );
} else if ( $value instanceof JsonLdNode ) {
$this->setProperty( $expandedName, $value->asObject() );
$expandedName = $this->expandName( $name );
if ( $expandedName === '@id' && ! $this->isBlankNode() ) {
throw new InvalidArgumentException( 'This node already has an id.' );
}
$expandedValue = $this->expandValue( $expandedName, $value );
$this->expanded->$expandedName = $expandedValue;
if ( $expandedName === '@id' ) {
$this->graph->nameBlankNode( $this->getId(), $expandedValue );
$this->id = $expandedValue;
}
}
public function add( $name, $value )
{
$expandedName = $this->expandName( $name );
if ( $expandedName === '@id' ) {
throw new InvalidArgumentException( 'Cannot add to the @id property.' );
}
$expandedValue = $this->expandValue( $expandedName, $value );
if ( property_exists( $this->expanded, $expandedName ) ) {
$this->expanded->$expandedName = array_merge( $this->expanded->$expandedName, $expandedValue );
} else {
$this->node->setProperty( $expandedName, $value );
$this->expanded->$expandedName = $expandedValue;
}
}
/**
* Convenience wrapper around $this->setProperty().
* Convenience wrapper around $this->set().
* If the property already exists, the new value overwrites the old value(s).
* @param string $name
* @param string|\stdClass|array $value
* @param string|stdClass|array $value
*/
public function __set( $name, $value )
{
return $this->setProperty( $name, $value );
return $this->set( $name, $value );
}
public function has( $name )
{
$expandedName = $this->expandName( $name );
return property_exists( $this->expanded, $expandedName );
}
/**
* 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 $value
* Given an already-expanded name and the current context, expands value so that it can be stored in $expanded.
* @param string $expandedName
* @param string|stdClass|array $value
* @return array|stdClass
*/
public function addPropertyValue( $name, $value )
private function expandValue( $expandedName, $value )
{
$expandedName = $this->expand_name( $name );
if ( is_array( $value ) ) {
$err = "Can't add array value to a property. To add multiple values call addPropertyValue multiple times or use setProperty";
throw new InvalidArgumentException( $err );
} else if ( $value instanceof stdClass ) {
$newDoc = JsonLD::getDocument( $value );
$newNodes = $newDoc->getGraph()->getNodes();
$newNode = count( $newNodes ) > 0 ? $newNodes[0] : $this->graph->createNode();
$this->node->addPropertyValue( $expandedName, $newNode );
} else if ( $value instanceof JsonLdNode ) {
$this->addPropertyValue( $expandedName, $value->asObject() );
} else {
$this->node->addPropertyValue( $expandedName, $value );
}
$nameToValue = (object) array( '@context' => $this->context, $expandedName => $value );
$expanded = JsonLD::expand( $nameToValue )[0];
$expandedValue = $expanded->$expandedName;
return $expandedValue;
}
/**
* Clears the property named $name, if it exists.
* Clears the property named $name.
* @param string $name
*/
public function clearProperty( $name )
public function clear( $name )
{
return $this->setProperty( $name, null );
$expandedName = $this->expandName( $name );
unset( $this->expanded->expandedName );
}
/**
* Returns the node as an object.
* @return stdClass
*/
public function asObject()
{
return $this->node->toJsonLd();
return JsonLD::compact( $this->expanded, $this->context );
}
/**
* 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.
* Returns true if this node is a blank node (even if it has a temporary id).
* @return bool
*/
private function expand_name( $name )
public function isBlankNode()
{
$dummyObj = (object) array(
'@context' => $this->context,
$name => '_dummyValue',
);
$expanded = (array) JsonLD::expand( $dummyObj )[0];
return array_keys( $expanded )[0];
return property_exists( $this->expanded, '@id' );
}
/**
@ -265,11 +277,11 @@ class JsonLdNode implements ArrayAccess
*/
public function offsetExists( $offset )
{
$expandedName = $this->expand_name( (string) $offset );
return !is_null( $this->node->getProperty( $expandedName ) );
return property_exists( $this->expanded, (string) $offset );
}
/**
* A convenience wrapper around $this->get(). Cardinality-one.
* Offset to retrieve
* @link https://php.net/manual/en/arrayaccess.offsetget.php
* @param mixed $offset <p>
@ -298,7 +310,7 @@ class JsonLdNode implements ArrayAccess
*/
public function offsetSet( $offset, $value )
{
return $this->setProperty( (string) $offset, $value );
$this->set( (string) $offset, $value );
}
/**
@ -312,6 +324,22 @@ class JsonLdNode implements ArrayAccess
*/
public function offsetUnset( $offset )
{
return $this->clearProperty( (string) $offset );
$this->clear( (string) $offset );
}
/**
* 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 expandName( $name )
{
// TODO memoize this function
$dummyObj = (object) array(
'@context' => $this->context,
$name => '_dummyValue',
);
$expanded = (array) JsonLD::expand( $dummyObj )[0];
return array_keys( $expanded )[0];
}
}

View File

@ -31,11 +31,15 @@ class JsonLdNodeFactory
/**
* Construct and return a new JsonLdNode.
* @param Node|\stdClass $jsonLd The JSON-LD object input
* @param Node|\stdClass $jsonLd The JSON-LD object input.
* @param JsonLdGraph|null $graph The JSON-LD graph.
* @return JsonLdNode
*/
public function newNode( $jsonLd )
public function newNode( $jsonLd, $graph = null )
{
return new JsonLdNode( $jsonLd, $this->context, $this, $this->dereferencer );
if ( is_null( $graph ) ) {
$graph = new JsonLdGraph();
}
return new JsonLdNode( $jsonLd, $this->context, $this, $this->dereferencer, $graph );
}
}

View File

@ -4,6 +4,7 @@ namespace ActivityPub\Test\JsonLd;
use ActivityPub\JsonLd\Exceptions\PropertyNotDefinedException;
use ActivityPub\JsonLd\JsonLdNode;
use ActivityPub\JsonLd\JsonLdNodeFactory;
use ActivityPub\Test\TestConfig\APTestCase;
use stdClass;
@ -224,7 +225,7 @@ class JsonLdNodeTest extends APTestCase
if ( $expectedException ) {
$this->setExpectedException( $expectedException );
}
$node->setProperty( $propertyName, $newValue );
$node->set( $propertyName, $newValue );
$this->assertEquals( $expectedValue, $node->$getPropertyName );
}
@ -287,12 +288,56 @@ class JsonLdNodeTest extends APTestCase
if ( $expectedException ) {
$this->setExpectedException( $expectedException );
}
$node->addPropertyValue( $propertyName, $newValue );
$node->add( $propertyName, $newValue );
$this->assertEquals( $expectedValue, $node->getMany( $getPropertyName ) );
}
private function makeJsonLdNode( $inputObj, $context )
public function provideForGetLinkedNode()
{
return new JsonLdNode( $inputObj, $context );
return array(
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',
),
),
'object',
(object) array(
'@context' => array( 'https://www.w3.org/ns/activitystreams' ),
'id' => 'https://example.org/objects/1',
'type' => 'Note',
),
),
);
}
/**
* @dataProvider provideForGetLinkedNode
*/
public function testGetLinkedNode( $inputObj, $context, $nodeGraph, $propertyName, $expectedValue, $expectedException = null )
{
$node = $this->makeJsonLdNode( $inputObj, $context, $nodeGraph );
if ( $expectedException ) {
$this->setExpectedException( $expectedException );
}
$actualValue = $node->get( $propertyName );
if ( $actualValue instanceof JsonLdNode ) {
$actualValue = $actualValue->asObject();
}
$this->assertEquals( $expectedValue, $actualValue );
}
private function makeJsonLdNode( $inputObj, $context, $nodeGraph = array() )
{
$factory = new JsonLdNodeFactory( $context, new TestDereferencer( $nodeGraph ) );
return $factory->newNode( $inputObj );
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace ActivityPub\Test\JsonLd;
use ActivityPub\JsonLd\Dereferencer\DereferencerInterface;
use ActivityPub\JsonLd\Exceptions\NodeNotFoundException;
use stdClass;
class TestDereferencer implements DereferencerInterface
{
private $nodes;
public function __construct( $nodes )
{
$this->nodes = $nodes;
}
/**
* @param string $iri The IRI to dereference.
* @return stdClass|array The dereferenced node.
* @throws NodeNotFoundException If a node with the IRI could not be found.
*/
public function dereference( $iri )
{
if ( array_key_exists( $iri, $this->nodes ) ) {
return $this->nodes[$iri];
} else {
throw new NodeNotFoundException( $iri );
}
}
}