src/Context.php
<?php
namespace JsonBrowser;
/**
* Document context
*
* @internal
* @since 1.5.0
*
* @package baacode/json-browser
* @copyright (c) 2017-2018 Erayd LTD
* @author Steve Gilberd <steve@erayd.net>
* @license ISC
*/
class Context
{
/** Configuration options */
private $options = 0;
/** Decoded JSON document */
private $document = null;
/** Annotation document */
private $annotations = [];
/** Path to root location in document */
private $path = [];
/**
* Create a new instance
*
* @since 1.5.0
*
* @param mixed $document Reference to the target document
* @param int $options Configuration options (bitmask)
*/
public function __construct(&$document, int $options = 0)
{
$this->document = &$document;
$this->options = $options;
}
/**
* Get a new context with the provided path as the root
*
* @param array $path Array of path elements
* @return self New context with $root as the document root
*/
public function getSubtreeContext(array $path) : self
{
$subtree = clone $this;
$subtree->path = array_merge($this->path, $path);
$subtree->annotations = &$this->annotations;
return $subtree;
}
/**
* Check whether the value at a given path exists
*
* @since 1.5.0
*
* @param array $path Array of path elements
* @return bool Whether a value exists at the given path
*/
public function valueExists(array $path) : bool
{
$this->getValue($path, $exists);
return $exists;
}
/**
* Get the value at a given path
*
* @since 1.5.0
*
* @param array $path Array of path elements
* @param bool $exists Reference - set to true if the value exists, false otherwise
* @return mixed|null Value data, or null if value does not exist
*/
public function getValue(array $path, bool &$exists = null)
{
$path = array_merge($this->path, $path);
$target = $this->document;
// follow path to conclusion or return null if not found
while (count($path)) {
$element = array_shift($path);
if (is_array($target) && array_key_exists($element, $target)) {
$target = $target[$element];
} elseif (is_object($target) && property_exists($target, $element)) {
$target = $target->$element;
} else {
$exists = false;
return null;
}
}
$exists = true;
return $target;
}
/**
* Set the value at a given path
*
* @since 1.5.0
*
* @param array $path Array of path elements
* @param mixed $value Value data to set
* @param bool $padSparseArray Whether to left-pad sparse arrays with null values
*/
public function setValue(array $path, $value, bool $padSparseArray = false)
{
$path = array_merge($this->path, $path);
$target = &$this->document;
// follow path to conclusion and create missing elements
while (count($path)) {
$element = array_shift($path);
$this->promoteContainer($target, $element);
// step into child element
if (is_array($target)) {
// left-pad array with nulls
if ($padSparseArray) {
for ($i = 0; $i < $element; $i++) {
if (!array_key_exists($element, $target)) {
$target[$i] = null;
}
}
}
if (!array_key_exists($element, $target)) {
$target[$element] = [];
}
$target = &$target[$element];
} elseif (is_object($target)) {
if (!property_exists($target, $element)) {
$target->$element = [];
}
$target = &$target->$element;
} else {
throw new Exception(
JsonBrowser::ERR_INVALID_CONTAINER_TYPE,
'Invalid container type: %s',
gettype($target)
);
}
}
// set value of target
$target = $value;
}
/**
* Delete the value at a given path
*
* @since 1.5.0
*
* @param array $path Array of path elements
* @param bool $deleteEmpty Whether to delete empty containers
*/
public function deleteValue(array $path, bool $deleteEmpty = false)
{
$path = array_merge($this->path, $path);
$target = &$this->document;
$containerPath = [];
// follow path to conclusion or return early if not found
while (count($path) > 1) {
$element = $containerPath[] = array_shift($path);
if (is_array($target) && array_key_exists($element, $target)) {
$target = &$target[$element];
} elseif (is_object($target) && property_exists($target, $element)) {
$target = &$target->$element;
} else {
return;
}
}
if (count($path)) {
// unset the child element
if (is_array($target)) {
unset($target[array_shift($path)]);
} elseif (is_object($target)) {
unset($target->{array_shift($path)});
}
// recurse to delete empty containers
if ($deleteEmpty && !count((array)$target)) {
$this->deleteValue($containerPath, $deleteEmpty);
}
} else {
// we're at the root, so set the target to null rather than unsetting it
$target = null;
}
}
/**
* Get annotations for a given path
*
* @since 2.1.0
*
* @param array $path Array of path elements
* @param string $name Annotation name
* @return array Array of annotation values
*/
public function getAnnotations(array $path, string $name = null) : array
{
$path = Util::encodePointer(array_merge($this->path, $path));
if (!array_key_exists($path, $this->annotations)) {
return [];
}
if (!is_null($name)) {
return $this->annotations[$path][$name] ?? [];
} else {
return $this->annotations[$path];
}
}
/**
* Set an annotation for a given path
*
* @since 2.1.0
*
* @param array $path Array of path elements
* @param string $name Annotation name
* @param mixed $value Annotation value
* @param bool $clear Clear existing annotations with the same name
*/
public function setAnnotation(array $path, string $name, $value, bool $clear = false)
{
$path = Util::encodePointer(array_merge($this->path, $path));
if (!array_key_exists($path, $this->annotations)) {
$this->annotations[$path] = [];
}
if ($clear || !array_key_exists($name, $this->annotations[$path])) {
$this->annotations[$path][$name] = [];
}
$this->annotations[$path][$name][] = $value;
}
/**
* Promote container type as necessary to hold a child key
*
* @since 1.5.0
*
* @param mixed $container Target container
* @param mixed $key Intended key
*/
private function promoteContainer(&$container, $key)
{
// promote null to array
if (is_null($container)) {
$container = [];
}
// promote array to object if the key is not an integer
if (is_array($container) && !(is_numeric($key) && $key == floor($key))) {
$container = (object)$container;
}
}
}