3

I need to convert an XML document into JSON, in order to easily access the data in JavaScript. I am currently using this method to convert the XML into JSON:

json_encode(new SimpleXMLElement($xml, LIBXML_NOCDATA));

However, I ran into a problem when an element only contains 1 child element. When it is parsed with SimpleXML, it is treated as an object instead of an array. I want them to always be treated as arrays, unless the element only contains text.

Example:

$xml = <<<END
<xml>
  <TESTS>
    <TEST>TEXT HERE</TEST>
  </TESTS>
</xml>
END;

echo json_encode(new SimpleXMLElement($xml, LIBXML_NOCDATA));

This outputs:

{"TESTS":{"TEST":"TEXT HERE"}}

If I add another element under , then the output is how I want it:

$xml = <<<END
<xml>
  <TESTS>
    <TEST>TEXT HERE</TEST>
    <TEST>MORE TEXT HERE</TEST>
  </TESTS>
</xml>
END;

echo json_encode(new SimpleXMLElement($xml, LIBXML_NOCDATA));

Output:

{"TESTS":{"TEST":["TEXT HERE","TEXT HERE"]}}

Note how the elements are contained inside a JSON array instead of a JSON object. Is there a way to force elements to be parsed as arrays?

hakre
  • 193,403
  • 52
  • 435
  • 836
David Barnes
  • 2,138
  • 5
  • 19
  • 25
  • It seems that you have object anyway but the value of this element in the first case is string but in the second is array. – Andrej Aug 24 '11 at 19:44
  • I think you'll have to walk the XML and manually convert to arrays. Even if there is a way to force arrays instead of objects, that first example will end up with this for JSON: {"Tests":[{"Test":["Text HERE"]}]}. I'm thinking that's not what you really want. – Tim Gautier Aug 24 '11 at 19:47
  • you're passing in an object (of class simplexml)... of course you're going to get an object out of json_encode. – Marc B Aug 24 '11 at 19:58
  • @Marc B I think the OP is looking for the array analagous of the JSON_FORCE_OBJECT option – brian_d Aug 24 '11 at 20:15
  • It seems that your second example - SimpleXMLElement falling back on an array for representing the nodes - happens just because there is no alternative. You can't have two properties named TEST in the same object. So SimpleXML uses an array as an act of desperation. With one item, that just won't happen. So you'll probably have to pick apart the XML yourself, as the other commenters have already said. – hashchange Aug 31 '11 at 20:27
  • A related Q&A that shows [how to use the JsonSerializeable interface with SimpleXMLElement to do this: *"PHP convert XML to JSON group when there is one child"*](http://stackoverflow.com/a/16938322/367456) – hakre Jul 08 '13 at 08:20

2 Answers2

2

I had to deal with the same situation. Here is a quick first solution to the problem - works fine for your example.

class JsonWithArrays
{
    protected $root, $callback;

    public function __construct( SimpleXMLElement $root )
    {
        $this->root = $root;
    }

    /**
     * Set a callback to return if a node should be represented as an array
     * under any circumstances.
     *
     * The callback receives two parameters to react to: the SimpleXMLNode in
     * question, and the nesting level of that node.
     */
    public function use_callback_forcing_array ( $callback )
    {
        $this->callback = $callback;
        return $this;
    }

    public function to_json ()
    {
        $transformed = $this->transform_subnodes( $this->root, new stdClass(), 0 );
        return json_encode( $transformed );
    }

    protected function transform_subnodes ( SimpleXMLElement $parent, stdClass $transformed_parent, $nesting_level )
    {
        $nesting_level++;

        foreach( $parent->children() as $node )
        {
            $name = $node->getName();
            $value = (string) $node;

            if ( count( $node->children() ) > 0 )
            {
                $transformed_parent->$name = new stdClass();
                $this->transform_subnodes( $node, $transformed_parent->$name, $nesting_level );
            }
            elseif ( count( $parent->$name ) > 1 or $this->force_array( $node, $nesting_level ) )
            {
                $transformed_parent->{$name}[] = $value;
            }
            else
            {
                $transformed_parent->$name = $value;
            }
        }

        return $transformed_parent;
    }

    protected function force_array ( $node, $nesting_level )
    {
        if ( is_callable( $this->callback ) )
        {
            return call_user_func( $this->callback, $node, $nesting_level );
        }
        else
        {
            return false;
        }
    }
}

$xml = <<<END
<xml> 
  <TESTS> 
    <TEST>TEXT HERE</TEST> 
  </TESTS> 
</xml> 
END;

$xml2 = <<<END
<xml> 
  <TESTS> 
    <TEST>TEXT HERE</TEST> 
    <TEST>MORE TEXT HERE</TEST> 
  </TESTS> 
</xml> 
END;

// Callback using the node name. Could just as well be done using the nesting
// level.
function cb_testnode_as_array( SimpleXMLElement $node, $nesting_level )
{
    return $node->getName() == 'TEST';
}

$transform = new JsonWithArrays( new SimpleXMLElement($xml, LIBXML_NOCDATA) );
echo $transform
    ->use_callback_forcing_array( 'cb_testnode_as_array' )
    ->to_json();

echo PHP_EOL;

$transform2 = new JsonWithArrays( new SimpleXMLElement($xml2, LIBXML_NOCDATA) );
echo $transform2
    ->use_callback_forcing_array( 'cb_testnode_as_array' )
    ->to_json();
hashchange
  • 7,029
  • 1
  • 45
  • 41
0
echo json_encode(json_decode(json_encode(new SimpleXMLElement($xml, LIBXML_NOCDATA)), true));

Really this is lot a stupid but you first if all convert to object then decode in array and convert to json like array style.. ))

ZigZag
  • 539
  • 1
  • 8
  • 19