Permalink
Fetching contributors…
Cannot retrieve contributors at this time
510 lines (424 sloc) 13 KB
<?php
/*
* (c) Markus Lanthaler <mail@markus-lanthaler.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace ML\JsonLD;
use stdClass as Object;
/**
* A Node represents a node in a JSON-LD graph.
*
* @author Markus Lanthaler <mail@markus-lanthaler.com>
*/
class Node implements NodeInterface, JsonLdSerializable
{
/** The @type constant. */
const TYPE = '@type';
/**
* @var GraphInterface The graph the node belongs to.
*/
private $graph;
/**
* @var string The ID of the node
*/
private $id;
/**
* @var array An associative array holding all properties of the node except it's ID
*/
private $properties = array();
/**
* An associative array holding all reverse properties of this node, i.e.,
* a pointers to all nodes that link to this node.
*
* @var array
*/
private $revProperties = array();
/**
* Constructor
*
* @param GraphInterface $graph The graph the node belongs to.
* @param null|string $id The ID of the node.
*/
public function __construct(GraphInterface $graph, $id = null)
{
$this->graph = $graph;
$this->id = $id;
}
/**
* {@inheritdoc}
*/
public function getId()
{
return $this->id;
}
/**
* {@inheritdoc}
*/
public function setType($type)
{
if ((null !== $type) && !($type instanceof NodeInterface)) {
if (is_array($type)) {
foreach ($type as $val) {
if ((null !== $val) && !($val instanceof NodeInterface)) {
throw new \InvalidArgumentException('type must be null, a Node, or an array of Nodes');
}
}
} else {
throw new \InvalidArgumentException('type must be null, a Node, or an array of Nodes');
}
}
$this->setProperty(self::TYPE, $type);
return $this;
}
/**
* {@inheritdoc}
*/
public function addType(NodeInterface $type)
{
$this->addPropertyValue(self::TYPE, $type);
return $this;
}
/**
* {@inheritdoc}
*/
public function removeType(NodeInterface $type)
{
$this->removePropertyValue(self::TYPE, $type);
return $this;
}
/**
* {@inheritdoc}
*/
public function getType()
{
return $this->getProperty(self::TYPE);
}
/**
* {@inheritdoc}
*/
public function getNodesWithThisType()
{
if (null === ($nodes = $this->getReverseProperty(self::TYPE))) {
return array();
}
return (is_array($nodes)) ? $nodes : array($nodes);
}
/**
* {@inheritdoc}
*/
public function getGraph()
{
return $this->graph;
}
/**
* {@inheritdoc}
*/
public function removeFromGraph()
{
// Remove other node's properties and reverse properties pointing to
// this node
foreach ($this->revProperties as $property => $nodes) {
foreach ($nodes as $node) {
$node->removePropertyValue($property, $this);
}
}
foreach ($this->properties as $property => $values) {
if (!is_array($values)) {
$values = array($values);
}
foreach ($values as $value) {
if ($value instanceof NodeInterface) {
$this->removePropertyValue($property, $value);
}
}
}
$g = $this->graph;
$this->graph = null;
$g->removeNode($this);
return $this;
}
/**
* {@inheritdoc}
*/
public function isBlankNode()
{
return ((null === $this->id) || ('_:' === substr($this->id, 0, 2)));
}
/**
* {@inheritdoc}
*/
public function setProperty($property, $value)
{
if (null === $value) {
$this->removeProperty($property);
} else {
$this->doMergeIntoProperty((string) $property, array(), $value);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function addPropertyValue($property, $value)
{
$existing = (isset($this->properties[(string) $property]))
? $this->properties[(string) $property]
: array();
if (!is_array($existing)) {
$existing = array($existing);
}
$this->doMergeIntoProperty((string) $property, $existing, $value);
return $this;
}
/**
* Merge a value into a set of existing values.
*
* @param string $property The name of the property.
* @param array $existingValues The existing values.
* @param mixed $value The value to merge into the existing
* values. This MUST NOT be an array.
*
* @throws \InvalidArgumentException If value is an array or an object
* which is neither a language-tagged
* string nor a typed value or a node.
*/
private function doMergeIntoProperty($property, $existingValues, $value)
{
// TODO: Handle lists!
if (null === $value) {
return;
}
if (!$this->isValidPropertyValue($value)) {
throw new \InvalidArgumentException(
'value must be a scalar, a node, a language-tagged string, or a typed value'
);
}
$normalizedValue = $this->normalizePropertyValue($value);
foreach ($existingValues as $existing) {
if ($this->equalValues($existing, $normalizedValue)) {
return;
}
}
$existingValues[] = $normalizedValue;
if (1 === count($existingValues)) {
$existingValues = $existingValues[0];
}
$this->properties[$property] = $existingValues;
if ($normalizedValue instanceof NodeInterface) {
$value->addReverseProperty($property, $this);
}
}
/**
* {@inheritdoc}
*/
public function removeProperty($property)
{
if (!isset($this->properties[(string) $property])) {
return $this;
}
$values = is_array($this->properties[(string) $property])
? $this->properties[(string) $property]
: array($this->properties[(string) $property]);
foreach ($values as $value) {
if ($value instanceof NodeInterface) {
$value->removeReverseProperty((string) $property, $this);
}
}
unset($this->properties[(string) $property]);
return $this;
}
/**
* {@inheritdoc}
*/
public function removePropertyValue($property, $value)
{
if (!$this->isValidPropertyValue($value) || !isset($this->properties[(string) $property])) {
return $this;
}
$normalizedValue = $this->normalizePropertyValue($value);
$values =& $this->properties[(string) $property];
if (!is_array($this->properties[(string) $property])) {
$values = array($values);
}
for ($i = 0, $length = count($values); $i < $length; $i++) {
if ($this->equalValues($values[$i], $normalizedValue)) {
if ($normalizedValue instanceof NodeInterface) {
$normalizedValue->removeReverseProperty((string) $property, $this);
}
unset($values[$i]);
break;
}
}
if (0 === count($values)) {
unset($this->properties[(string) $property]);
return $this;
}
$this->properties[(string) $property] = array_values($values); // re-index the array
if (1 === count($this->properties[(string) $property])) {
$this->properties[(string) $property] = $this->properties[(string) $property][0];
}
}
/**
* {@inheritdoc}
*/
public function getProperties()
{
return $this->properties;
}
/**
* {@inheritdoc}
*/
public function getProperty($property)
{
return (isset($this->properties[(string) $property]))
? $this->properties[(string) $property]
: null;
}
/**
* {@inheritdoc}
*/
public function getReverseProperties()
{
$result = array();
foreach ($this->revProperties as $key => $nodes) {
$result[$key] = array_values($nodes);
}
return $result;
}
/**
* {@inheritdoc}
*/
public function getReverseProperty($property)
{
if (!isset($this->revProperties[(string) $property])) {
return null;
}
$result = array_values($this->revProperties[(string) $property]);
return (1 === count($result))
? $result[0]
: $result;
}
/**
* {@inheritdoc}
*/
public function equals(NodeInterface $other)
{
return $this === $other;
}
/**
* {@inheritdoc}
*/
public function toJsonLd($useNativeTypes = true)
{
$node = new \stdClass();
// Only label blank nodes if other nodes point to it
if ((false === $this->isBlankNode()) || (count($this->getReverseProperties()) > 0)) {
$node->{'@id'} = $this->getId();
}
$properties = $this->getProperties();
foreach ($properties as $prop => $values) {
if (false === is_array($values)) {
$values = array($values);
}
if (self::TYPE === $prop) {
$node->{'@type'} = array();
foreach ($values as $val) {
$node->{'@type'}[] = $val->getId();
}
continue;
}
$node->{$prop} = array();
foreach ($values as $value) {
if ($value instanceof NodeInterface) {
$ref = new \stdClass();
$ref->{'@id'} = $value->getId();
$node->{$prop}[] = $ref;
} elseif (is_object($value)) { // language-tagged string or typed value
$node->{$prop}[] = $value->toJsonLd($useNativeTypes);
} else {
$val = new Object();
$val->{'@value'} = $value;
$node->{$prop}[] = $val;
}
}
}
return $node;
}
/**
* Add a reverse property.
*
* @param string $property The name of the property.
* @param NodeInterface $node The node which has a property pointing
* to this node instance.
*/
protected function addReverseProperty($property, NodeInterface $node)
{
$this->revProperties[$property][$node->getId()] = $node;
}
/**
* Remove a reverse property.
*
* @param string $property The name of the property.
* @param NodeInterface $node The node which has a property pointing
* to this node instance.
*/
protected function removeReverseProperty($property, NodeInterface $node)
{
unset($this->revProperties[$property][$node->getId()]);
if (0 === count($this->revProperties[$property])) {
unset($this->revProperties[$property]);
}
}
/**
* Checks whether a value is a valid property value.
*
* @param mixed $value The value to check.
*
* @return bool Returns true if the value is a valid property value;
* false otherwise.
*/
protected function isValidPropertyValue($value)
{
return (is_scalar($value) ||
(is_object($value) &&
((($value instanceof NodeInterface) && ($value->getGraph() === $this->graph)) ||
($value instanceof Value))));
}
/**
* Normalizes a property value by converting scalars to Value objects.
*
* @param mixed $value The value to normalize.
*
* @return NodeInterface|Value The normalized value.
*/
protected function normalizePropertyValue($value)
{
if (false === is_scalar($value)) {
return $value;
}
return Value::fromJsonLd((object) array('@value' => $value));
}
/**
* Checks whether the two specified values are the same.
*
* Scalars and nodes are checked for identity, value objects for
* equality.
*
* @param mixed $value1 Value 1.
* @param mixed $value2 Value 2.
*
* @return bool Returns true if the two values are equals; otherwise false.
*/
protected function equalValues($value1, $value2)
{
if (gettype($value1) !== gettype($value2)) {
return false;
}
if (is_object($value1) && ($value1 instanceof Value)) {
return $value1->equals($value2);
}
return ($value1 === $value2);
}
}