Use JSONPath to set a value in an array - php

I'm trying to process some decoded json data using the PHP implementation of JSONPath (http://goessner.net/articles/JsonPath/).
I'm fine with using an expression to FIND data in the decoded JSON, but I'd like to be able to SET data using a JSONPath expression. Has anyone been able to do this in PHP using JSONPath, if so, how?

It seems that this implementation of JSONPath does not support set operations.
I've written a simple function that can be added to jsonPath.php to add this functionality. I've pasted it here in case it might be of use to anyone else:
/**
* #param array $obj Decoded json file to alter
* #param string $expr JSONPath expression (http://goessner.net/articles/JsonPath/)
* #param mixed $value Value to set all matching entries to
*/
function jsonPathSet(&$obj, $expr, $value)
{
$paths = jsonPath($obj, $expr, array('resultType' => 'PATH'));
$jsonPath = new JsonPath();
foreach ($paths as $path) {
$p = $jsonPath->normalize($path);
$keys = explode(';', $p);
$current = &$obj;
foreach ($keys as $key) {
if($key=='$') {
continue;
} else if (is_array($current)) {
$current = &$current[$key];
} else {
$current = &$current->$key;
}
}
$current = $value;
}
}
Thanks to Mike Brant for the suggestions!

In briefly looking at the documentation, it would appear that JSONPath doesn't support set operations. If one were so inclined, I would imagine that you could modify JSONPath to optionally return an array of pointers (i.e. object references) as a resultType such that you could operate on the values directly.

Related

How can I make big multidimensional variables seem shorter in PHP?

I'm creating a php class which is getting slightly out of hand the deeper it gets.
Here's an example:
unset($this->file[$key]->inspect->formats);
unset($this->file[$key]->inspect->tags);
unset($this->file[$key]->inspect->chapters);
unset($this->file[$key]->inspect->annotations);
unset($this->file[$key]->inspect->automatic_captions);
unset($this->file[$key]->inspect->subtitles);
$this->file[$key]->inspect->name = trim($this->file[$key]->inspect->name);
$this->file[$key]->inspect->artist = trim($this->file[$key]->inspect->artist);
Instead of writing $this->file[$key]->inspect for every single variable I want to use is there a way I can set a variable e.g $inspect to take this place?
So that when I write $inspect->subtitles it'll know what I really mean and affect the main $this->file[$key]->inspect->subtitles?
$inspect = &$this->file[$key]->inspect;
declare this. Now you can set your data like this
$inspect->formats = 'format';
$inspect->subs = 'subs';
// ...
adding the & you will affect the variable and not only a copy of this variable
here are explanations about references http://php.net/manual/en/language.references.whatare.php
One approach you could use is replicate the object_get() helper method from Laravel that will fetch elements based on dot notation.
/**
* Get an item from an object using "dot" notation.
*
* #param object $object
* #param string $key
* #param mixed $default
* #return mixed
*/
function object_get($object, $key, $default = null)
{
if (is_null($key) || trim($key) == '') return $object;
foreach (explode('.', $key) as $segment)
{
if ( ! is_object($object) || ! isset($object->{$segment}))
{
return value($default);
}
$object = $object->{$segment};
}
return $object;
}
$ob = new StdClass();
$ob->property->name->value = 'Lol';
echo object_get($ob, 'property.name.value');
Unfortunately there'd be a bit of extra implementation if the $object->property was an array like in your example.

How do I dynamically create a PHP SimpleXMLElement Object while keeping current properties?

I am reading in a an xml file which returns me a SimpleXMLElement Object representation of the xml. I am going to take an array and feed in new values to that object. I don't know what I am going to be in that array.
if I were to brute force this I would do something like this.
//Solution 1: Brute Force
//Just creating an array and value for purposes of demonstration.
$arOfData = array( [0]=>"theFirstNode", [1]=>"theSecondNode",[2]=>"theThirdNode" );
$value = "The XML Node Value";
$simpleXml->$arOfData[0]->$arOfData[1]->$arOfData[2] = $value;
//The next best thing I can think of doing is something like this.
//Solution 2: Semi-brute force
//
foreach($this->arrayData as $key => $value) {
$xmlNodes = explode( '-', $key);
$numNodes = count($xmlNodes);
switch($numNodes) {
case 1:
$simpleXml->$xmlNodes[0] = $value;
break;
case 2:
$simpleXml->$xmlNodes[0]->$xmlNodes[1] = $value;
break;
case 3:
$simpleXml->$xmlNodes[0]->$xmlNodes[1]->$xmlNodes[2] = $value;
break;
case 4:
$simpleXml->$xmlNodes[0]->$xmlNodes[1]->$xmlNodes[2]->$xmlNodes[3] = $value;
break;
case 5:
$simpleXml->$xmlNodes[0]->$xmlNodes[1]->$xmlNodes[2]->$xmlNodes[3]->$xmlNodes[4] = $value;
break;
}
}
*note This solution uses the array key and explodes it to an array delimited by a dash and then uses the array value as the new xml value. So don't let that distract you.
The problem with solution #2 is: what happens when we get a xml node that is deeper than 5? Its not going to be stuffed into our new object we are creating. Oh oh. It's also not very elegant ;). I am not sure how to do this in a more recursive manner.
Like you already wrote in your question, you need to have this dynamically because you do not know about the number of parent elements.
You need to dig a little deeper into how simpexml works to get this done.
But first let me suggest you to have a different notation, not with the minus sign you have but with a slash like in a path.
first/second/third
This is also common with Xpath and I think it's pretty well speaking for itself. Also the minus sign can be part of an element name, but the slash can not. So this is just a bit better.
Before I show you how you can easily access that <third> element node to set its value, first lets look at some assignment basics in simplexml.
To access and set this element-node in a SimpleXMLElement see the following example:
$xml = new SimpleXMLElement('<root><first><second><third/></second></first></root>');
$element = $xml->first->second->third;
$element[0] = "value";
This is pretty straight forward but you can see two things here:
The <third> element already exists in the document.
The code uses as simplexml-self-reference ([0]) which allows to set the XML value of the element variable (and not the variable). This is specific to how SimpleXMLElement works.
The second point also contains the solution to the problem how to deal with non-existent elements. $element[0] is NULL in case the element does not exists:
$xml = new SimpleXMLElement('<root><first><second/></first></root>');
$element = $xml->first->second->third;
var_dump($element[0]); # NULL
So let's try to conditionally add the third element in case it does not exists:
if ($xml->first->second->third[0] === NULL) {
$xml->first->second->third = "";
}
This does solve that problem. So the only thing left to do is to do that in an iterative fashion for all parts of the path:
first/second/third
To keep this easy, create a function for this:
/**
* Modify an elements value specified by a string-path.
*
* #param SimpleXMLElement $parent
* #param string $path
* #param string $value (optional)
*
* #return SimpleXMLElement the modified element-node
*/
function simplexml_deep_set(SimpleXMLElement $parent, $path, $value = '')
{
### <mocked> to be removed later: ###
if ($parent->first->second->third[0] === NULL) {
$parent->first->second->third = "";
}
$element = $parent->first->second->third;
### </mocked> ###
$element[0] = $value;
return $element;
}
Because the function is mocked, it can be used directly:
$xml = new SimpleXMLElement('<root><first><second/></first></root>');
simplexml_deep_set($xml, "first/second/third", "The XML Node Value");
$xml->asXML('php://output');
And this works:
<?xml version="1.0"?>
<root><first><second><third>The XML Node Value</third></second></first></root>
So now removing the mock. First insert the explode like you have it as well. Then all that needs to be done is to go along each step of the path and create the element conditionally if it yet does not exist. In the end $element will be the element to modify:
$steps = explode('/', $path);
$element = $parent;
foreach ($steps as $step)
{
if ($element->{$step}[0] === NULL) {
$element->$step = '';
}
$element = $element->$step;
}
This foreach is needed to replace the mock with a working version. Compare with the full function definition at a glance:
function simplexml_deep_set(SimpleXMLElement $parent, $path, $value = '')
{
$steps = explode('/', $path);
$element = $parent;
foreach ($steps as $step)
{
if ($element->{$step}[0] === NULL) {
$element->$step = "";
}
$element = $element->$step;
}
$element[0] = $value;
return $element;
}
Lets modify more crazy things to test it out:
$xml = new SimpleXMLElement('<root><first><second/></first></root>');
simplexml_deep_set($xml, "first/second/third", "The XML Node Value");
simplexml_deep_set(
$xml, "How/do/I/dynamically/create/a/php/simplexml/object/while/keeping/current/properties"
, "The other XML Node Value"
);
$xml->asXML('php://output');
Example-Output (beautified):
<?xml version="1.0"?>
<root>
<first>
<second>
<third>The XML Node Value</third>
</second>
</first>
<How>
<do>
<I>
<dynamically>
<create>
<a>
<php>
<simplexml>
<object>
<while>
<keeping>
<current>
<properties>The other XML Node Value</properties>
</current>
</keeping>
</while>
</object>
</simplexml>
</php>
</a>
</create>
</dynamically>
</I>
</do>
</How>
</root>
See it in action.

json_encode arrays with iso-8895 characters

I have a quite complex, even though not really big, array, with many levels of nesting.
The array contains values that are encoded in ISO-8895, and also objects, with the same issue.
If I just
json_encode($array)
PHP wil silently encode all the values contining ISO-8895 characters as null.
Looking at the PHP documentation, I managed to write a working solution:
function fixMultibyteSerializedObject($match)
{
return 's:' . mb_strlen($match[2]);
}
/**
* Useful to json-encode arrays of objects with ISO-8895 encoded values.
* Does not work with iso-encoded keys
* #param var $object array or object to be encoded
* #param int $options json_encode options
*/
function isoJsonEncode($object, $options = null)
{
$str = serialize($object);
$str = mb_convert_encoding($str, 'utf-8');
$str = preg_replace_callback(
'!(?<=^|;)s:(\d+)(?=:"(.*?)";(?:}|a:|s:|b:|d:|i:|o:|N;))!s',
'fixMultibyteSerializedObject',
$str);
$object = unserialize($str);
return json_encode($object, $options);
}
Apart from getting a better library, such as the Zend json encoding component, can you suggest a better solution?
Thank you,
Iacopo
What about something like this?
array_walk_recursive($array, function (&$elem) {
if (is_string($elem)) {
$elem = iconv('ISO-8895', 'UTF-8', $elem);
}
});
echo json_encode($array);

php -- combining arrays

So I'm trying to write a function that does the following: I have about 20 or so XML files (someday I will have over a hundred) and in the header of each file is the name of a person who was a peer review editor <editor role="PeerReviewEditor">John Doe</editor>. I want to run through the directory where these files are stored and capture the name of the Peer-Review-Editor for that file. I want to end up with an variable $reviewEditorNames that contains all of the different names. (I will then use this to display a list of editors, etc.)
Here's what I've got so far. I'm worried about the last part. I feel like the attempt to turn $editorReviewName into $editorReviewNames is not going to combine the individuals for each file, but an array found within a given file (even if there is only one name in a given file, and thus it is an array of 1)
I'm grateful for your help.
function editorlist()
{
$filename = readDirectory('../editedtranscriptions');
foreach($filename as $file)
{
$xmldoc = simplexml_load_file("../editedtranscriptions/$file");
$xmldoc->registerXPathNamespace("tei", "http://www.tei-c.org/ns/1.0");
$reviewEditorName = $xmldoc->xpath("//tei:editor[#role='PeerReviewEditor']");
return $reviewEditorNames[] = $reviewEditorName;
}
}
I would put things more apart, that helps as well when you need to change your code later on.
Next to that, you need to check the return of the xpath, most likely you want to process only the first match (is there one editor per file?) and you want to return it as string.
If you put things into functions of it's own it's more easy to make a function to only do one thing and so it's easier to debug and improve things. E.g. you can first test if a editorFromFile function does what it should and then run it on multiple files:
/**
* get PeerReviewEditor from file
*
* #param string $file
* #return string
*/
function editorFromFile($file)
{
$xmldoc = simplexml_load_file($file);
$xmldoc->registerXPathNamespace("tei", "http://www.tei-c.org/ns/1.0");
$node = $xmldoc->xpath("//tei:editor[#role='PeerReviewEditor'][1]");
return (string) $node[0];
}
/**
* get editors from a path
*
* #param string $path
* #return array
*/
function editorlist($path)
{
$editors = array();
$files = glob(sprintf('%s/*.xml', $path), GLOB_NOSORT);
foreach($files as $file)
{
$editors[] = editorFromFile($file);
}
return $editors;
}
Just a little update:
function editorlist() {
$reviewEditorNames = array(); // init the array
$filename = readDirectory('../editedtranscriptions');
foreach($filename as $file) {
$xmldoc = simplexml_load_file("../editedtranscriptions/$file");
$xmldoc->registerXPathNamespace("tei", "http://www.tei-c.org/ns/1.0");
// add to the array
$result = $xmldoc->xpath("//tei:editor[#role='PeerReviewEditor']");
if (sizeof($result) > 0) {
$reviewEditorNames[] = (string)$result[0];
}
}
// return the array
return $reviewEditorNames;
}

PHP - recursive Array to Object?

Is there a way to convert a multidimensional array to a stdClass object in PHP?
Casting as (object) doesn't seem to work recursively. json_decode(json_encode($array)) produces the result I'm looking for, but there has to be a better way...
As far as I can tell, there is no prebuilt solution for this, so you can just roll your own:
function array_to_object($array) {
$obj = new stdClass();
foreach ($array as $k => $v) {
if (strlen($k)) {
if (is_array($v)) {
$obj->{$k} = array_to_object($v); //RECURSION
} else {
$obj->{$k} = $v;
}
}
}
return $obj;
}
I know this answer is coming late but I'll post it for anyone who's looking for a solution.
Instead of all this looping etc, you can use PHP's native json_* function. I've got a couple of handy functions that I use a lot
/**
* Convert an array into a stdClass()
*
* #param array $array The array we want to convert
*
* #return object
*/
function arrayToObject($array)
{
// First we convert the array to a json string
$json = json_encode($array);
// The we convert the json string to a stdClass()
$object = json_decode($json);
return $object;
}
/**
* Convert a object to an array
*
* #param object $object The object we want to convert
*
* #return array
*/
function objectToArray($object)
{
// First we convert the object into a json string
$json = json_encode($object);
// Then we convert the json string to an array
$array = json_decode($json, true);
return $array;
}
Hope this can be helpful
You and many others have pointed to the JSON built-in functions, json_decode() and json_encode(). The method which you have mentioned works, but not completely: it won't convert indexed arrays to objects, and they will remain as indexed arrays. However, there is a trick to overcome this problem. You can use JSON_FORCE_OBJECT constant:
// Converts an array to an object recursively
$object = json_decode(json_encode($array, JSON_FORCE_OBJECT));
Tip: Also, as mentioned here, you can convert an object to array recursively using JSON functions:
// Converts an object to an array recursively
$array = json_decode(json_encode($object), true));
Important Note: If you do care about performance, do not use this method. While it is short and clean, but it is the slowest among alternatives. See my other answer in this thread relating this.
function toObject($array) {
$obj = new stdClass();
foreach ($array as $key => $val) {
$obj->$key = is_array($val) ? toObject($val) : $val;
}
return $obj;
}
You can use the array_map recursively:
public static function _arrayToObject($array) {
return is_array($array) ? (object) array_map([__CLASS__, __METHOD__], $array) : $array;
}
Works perfect for me since it doesn't cast for example Carbon objects to a basic stdClass (which the json encode/decode does)
/**
* Recursively converts associative arrays to stdClass while keeping integer keys subarrays as arrays
* (lists of scalar values or collection of objects).
*/
function a2o( array $array ) {
$resultObj = new \stdClass;
$resultArr = array();
$hasIntKeys = false;
$hasStrKeys = false;
foreach ( $array as $k => $v ) {
if ( !$hasIntKeys ) {
$hasIntKeys = is_int( $k );
}
if ( !$hasStrKeys ) {
$hasStrKeys = is_string( $k );
}
if ( $hasIntKeys && $hasStrKeys ) {
$e = new \Exception( 'Current level has both integer and string keys, thus it is impossible to keep array or convert to object' );
$e->vars = array( 'level' => $array );
throw $e;
}
if ( $hasStrKeys ) {
$resultObj->{$k} = is_array( $v ) ? a2o( $v ) : $v;
} else {
$resultArr[$k] = is_array( $v ) ? a2o( $v ) : $v;
}
}
return ($hasStrKeys) ? $resultObj : $resultArr;
}
Some of the other solutions posted here fail to tell apart sequential arrays (what would be [] in JS) from maps ({} in JS.) For many use cases it's important to tell apart PHP arrays that have all sequential numeric keys, which should be left as such, from PHP arrays that have no numeric keys, which should be converted to objects. (My solutions below are undefined for arrays that don't fall in the above two categories.)
The json_decode(json_encode($x)) method does handle the two types correctly, but is not the fastest solution. It's still decent though, totaling 25µs per run on my sample data (averaged over 1M runs, minus the loop overhead.)
I benchmarked a couple of variations of the recursive converter and ended up with the following. It rebuilds all arrays and objects (performing a deep copy) but seems to be faster than alternative solutions that modify the arrays in place. It clocks at 11µs per execution on my sample data:
function array_to_object($x) {
if (!is_array($x)) {
return $x;
} elseif (is_numeric(key($x))) {
return array_map(__FUNCTION__, $x);
} else {
return (object) array_map(__FUNCTION__, $x);
}
}
Here is an in-place version. It may be faster on some large input data where only small parts need to be converted, but on my sample data it took 15µs per execution:
function array_to_object_inplace(&$x) {
if (!is_array($x)) {
return;
}
array_walk($x, __FUNCTION__);
reset($x);
if (!is_numeric(key($x))) {
$x = (object) $x;
}
}
I did not try out solutions using array_walk_recursive()
public static function _arrayToObject($array) {
$json = json_encode($array);
$object = json_decode($json);
return $object
}
Because the performance is mentioned, and in fact it should be important in many places, I tried to benchmark functions answered here.
You can see the code and sample data here in this gist. The results are tested with the data exists there (a random JSON file, around 200 KB in size), and each function repeated one thousand times, for the results to be more accurate.
Here are the results for different PHP configurations:
PHP 7.4.16 (no JIT)
$ php -dopcache.enable_cli=1 benchmark.php
pureRecursive(): Completed in 0.000560s
pureRecursivePreservingIntKeys(): Completed in 0.000580s
jsonEncode(): Completed in 0.002045s
jsonEncodeOptimized(): Completed in 0.002060s
jsonEncodeForceObject(): Completed in 0.002174s
arrayMap(): Completed in 0.000561s
arrayMapPreservingIntKeys(): Completed in 0.000592s
arrayWalkInplaceWrapper(): Completed in 0.001016s
PHP 8.0.2 (no JIT)
$ php -dopcache.enable_cli=1 benchmark.php
pureRecursive(): Completed in 0.000535s
pureRecursivePreservingIntKeys(): Completed in 0.000578s
jsonEncode(): Completed in 0.001991s
jsonEncodeOptimized(): Completed in 0.001990s
jsonEncodeForceObject(): Completed in 0.002164s
arrayMap(): Completed in 0.000579s
arrayMapPreservingIntKeys(): Completed in 0.000615s
arrayWalkInplaceWrapper(): Completed in 0.001040s
PHP 8.0.2 (tracing JIT)
$ php -dopcache.enable_cli=1 -dopcache.jit_buffer_size=250M -dopcache.jit=tracing benchmark.php
pureRecursive(): Completed in 0.000422s
pureRecursivePreservingIntKeys(): Completed in 0.000410s
jsonEncode(): Completed in 0.002004s
jsonEncodeOptimized(): Completed in 0.001997s
jsonEncodeForceObject(): Completed in 0.002094s
arrayMap(): Completed in 0.000577s
arrayMapPreservingIntKeys(): Completed in 0.000593s
arrayWalkInplaceWrapper(): Completed in 0.001012s
As you see, the fastest method with this benchmark is pure recursive PHP functions (posted by #JacobRelkin and #DmitriySintsov), especially when it comes to the JIT compiler. When it comes to json_* functions, they are the slowest ones. They are about 3x-4x (in the case of JIT, 5x) slower than the pure method, which may seem unbelievable.
One thing to note: If you remove iterations (i.e. run each function only one time), or even strictly lower its count, the results would differ. In such cases, arrayMap*() variants win over pureRecursive*() ones (still json_* functions method should be the slowest). But, you should simply ignore these cases. In the terms of performance, scalability is much more important.
As a result, in the case of converting arrays to object (and vice versa?), you should always use pure PHP functions, resulting in the best performance, perhaps independent from your configurations.
The simpliest way to convert an associative array to object is:
First encode it in json, then decode it.
like $objectArray = json_decode(json_encode($associtiveArray));
Here's a function to do an in-place deep array-to-object conversion that uses PHP internal (shallow) array-to-object type casting mechanism.
It creates new objects only when necessary, minimizing data duplication.
function toObject($array) {
foreach ($array as $key=>$value)
if (is_array($value))
$array[$key] = toObject($value);
return (object)$array;
}
Warning - do not use this code if there is a risk of having circular references.
Here is a smooth way to do it that can handle an associative array with great depth and doesn't overwrite object properties that are not in the array.
<?php
function setPropsViaArray( $a, $o )
{
foreach ( $a as $k => $v )
{
if ( is_array( $v ) )
{
$o->{$k} = setPropsViaArray( $v, ! empty ( $o->{$k} ) ? $o->{$k} : new stdClass() );
}
else
{
$o->{$k} = $v;
}
}
return $o;
};
setPropsViaArray( $newArrayData, $existingObject );
Late, but just wanted to mention that you can use the JSON encoding/decoding to convert fully from/to array:
//convert object $object into array
$array = json_decode(json_encode($object), true);
//convert array $array into object
$object = json_decode(json_encode($array));
json_encode and json_decode functions are available starting from php 5.2
EDIT: This function is conversion from object to array.
From https://forrst.com/posts/PHP_Recursive_Object_to_Array_good_for_handling-0ka
protected function object_to_array($obj)
{
$arrObj = is_object($obj) ? get_object_vars($obj) : $obj;
foreach ($arrObj as $key => $val) {
$val = (is_array($val) || is_object($val)) ? $this->object_to_array($val) : $val;
$arr[$key] = $val;
}
return $arr;
}
I was looking for a way that acts like json_decode(json_encode($array))
The problem with most other recursive functions here is that they also convert sequential arrays into objects. However, the JSON variant does not do this by default. It only converts associative arrays into objects.
The following implementation works for me like the JSON variant:
function is_array_assoc ($arr) {
if (!is_array($arr)) return false;
foreach (array_keys($arr) as $k => $v) if ($k !== $v) return true;
return false;
}
// json_decode(json_encode($array))
function array_to_object ($arr) {
if (!is_array($arr) && !is_object($arr)) return $arr;
$arr = array_map(__FUNCTION__, (array)$arr);
return is_array_assoc($arr) ? (object)$arr : $arr;
}
// json_decode(json_encode($array, true))
// json_decode(json_encode($array, JSON_OBJECT_AS_ARRAY))
function object_to_array ($obj) {
if (!is_object($obj) && !is_array($obj)) return $obj;
return array_map(__FUNCTION__, (array)$obj);
}
If you want to have the functions as a class:
class ArrayUtils {
public static function isArrAssoc ($arr) {
if (!is_array($arr)) return false;
foreach (array_keys($arr) as $k => $v) if ($k !== $v) return true;
return false;
}
// json_decode(json_encode($array))
public static function arrToObj ($arr) {
if (!is_array($arr) && !is_object($arr)) return $arr;
$arr = array_map([__CLASS__, __METHOD__], (array)$arr);
return self::isArrAssoc($arr) ? (object)$arr : $arr;
}
// json_decode(json_encode($array, true))
// json_decode(json_encode($array, JSON_OBJECT_AS_ARRAY))
public static function objToArr ($obj) {
if (!is_object($obj) && !is_array($obj)) return $obj;
return array_map([__CLASS__, __METHOD__], (array)$obj);
}
}
If anyone finds any mistakes please let me know.
/**
* Convert a multidimensional array to an object recursively.
* For any arrays inside another array, the result will be an array of objects.
*
* #author Marcos Freitas
* #param array|any $props
* #return array|any
*/
function array_to_object($props, $preserve_array_indexes = false) {
$obj = new \stdClass();
if (!is_array($props)) {
return $props;
}
foreach($props as $key => $value) {
if (is_numeric($key) && !$preserve_array_indexes) {
if(!is_array($obj)) {
$obj = [];
}
$obj[] = $this->array_to_object($value);
continue;
}
$obj->{$key} = is_array($value) ? $this->array_to_object($value) : $value;
}
return $obj;
}
The shortest I could come up with:
array_walk_recursive($obj, function (&$val) { if (is_object($val)) $val = get_object_vars($val); });

Categories